Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ jobs:
needs: check-branch
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python_version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
os:
- "ubuntu"
# - "macos"
Expand Down
18 changes: 9 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@ repos:
types:
- python

- id: vulture
name: vulture
entry: vulture --min-confidence 80
language: system
files: "./"
description: Find unused Python code.
pass_filenames: true
types:
- python
# - id: vulture
# name: vulture
# entry: vulture --min-confidence 80
# language: system
# files: "./"
# description: Find unused Python code.
# pass_filenames: true
# types:
# - python

- id: mccabe
name: mccabe
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
docs/changelog.md
CHANGELOG.md
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
`arx` stays compiler-only. `arxpm` owns project manifests (`arxproj.toml`),
workspace lifecycle, Pixi integration, and user-facing workflow commands.

## Compatibility

- Python 3.10+ is supported.
- On Python 3.10, `arxpm` uses `tomli` as a compatibility fallback for
`tomllib`.

## Architecture

- `models.py`: typed manifest models.
- `manifest.py`: `arxproj.toml` parsing and rendering.
- `_toml.py`: TOML parser compatibility shim (`tomllib`/`tomli`).
- `pixi.py`: Pixi adapter and `pixi.toml` handling.
- `project.py`: project workflows (`init`, `add`, `install`, `build`, `run`).
- `doctor.py`: health checks for environment and manifest.
Expand Down
4 changes: 3 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ arxpm init --directory ./my-project --no-pixi
Effects:

- creates `arxproj.toml`
- creates `src/main.arx`
- creates `src/main.x`
- optionally creates/updates `pixi.toml`

## `arxpm add`
Expand Down Expand Up @@ -58,6 +58,8 @@ arxpm run
arxpm run --directory examples
```

Build/compiler output and the application stdout/stderr are streamed directly;

## `arxpm doctor`

Report environment health and manifest status.
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This guide sets up `arxpm` for local development and smoke testing.

## Prerequisites

- Python 3.10+
- Conda or Mamba
- Poetry

Expand Down
7 changes: 7 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@
Arx projects use `arxproj.toml` as their project manifest. Python packaging is
only for distributing `arxpm` itself.

## Compatibility

- Python 3.10+ is supported.
- On Python 3.10, `arxpm` uses `tomli` as a compatibility fallback for
`tomllib`.

## Architecture

- `src/arxpm/models.py`: typed manifest models.
- `src/arxpm/manifest.py`: parse/render `arxproj.toml`.
- `src/arxpm/_toml.py`: TOML parser compatibility shim (`tomllib`/`tomli`).
- `src/arxpm/pixi.py`: Pixi detection and partial `pixi.toml` sync.
- `src/arxpm/project.py`: `init`, `add`, `install`, `build`, `run`.
- `src/arxpm/doctor.py`: environment and manifest checks.
Expand Down
2 changes: 1 addition & 1 deletion docs/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ version = "0.1.0"
edition = "2026"

[build]
entry = "src/main.arx"
entry = "src/main.x"
out_dir = "build"

[dependencies]
Expand Down
2 changes: 1 addition & 1 deletion examples/arxproj.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2026"

[build]
entry = "src/main.arx"
entry = "src/main.x"
out_dir = "build"

[dependencies]
Expand Down
3 changes: 0 additions & 3 deletions examples/src/main.arx

This file was deleted.

12 changes: 12 additions & 0 deletions examples/src/main.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
```
title: Simple main module
```

fn main() -> i32:
```
title: Print hello world
returns:
type: i32
```
print("Hello, Arx!");
return 0;
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ authors = [{ name = "Ivan Ogasawara" }]
dependencies = [
"typer>=0.12.3,<1.0.0",
"arxlang >= 0.3.3",
"tomli>=2.0.1; python_version < '3.11'",
]

[project.scripts]
Expand Down
12 changes: 12 additions & 0 deletions src/arxpm/_toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
title: TOML parser compatibility helpers.
"""

from __future__ import annotations

try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - exercised on Python <3.11
import tomli as tomllib # type: ignore[no-redef]

__all__ = ["tomllib"]
2 changes: 0 additions & 2 deletions src/arxpm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,6 @@ def run_command(
except ArxpmError as exc:
_fail(exc)

typer.echo("Run completed.")


@app.command()
def doctor(
Expand Down
7 changes: 7 additions & 0 deletions src/arxpm/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import annotations

import subprocess
import sys
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
Expand Down Expand Up @@ -89,6 +90,12 @@ def run_command(
stdout=completed.stdout,
stderr=completed.stderr,
)

if result.stdout:
print(result.stdout, end="")
if result.stderr:
print(result.stderr, end="", file=sys.stderr)

if check and result.returncode != 0:
raise ExternalCommandError(
result.command,
Expand Down
2 changes: 1 addition & 1 deletion src/arxpm/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from __future__ import annotations

import json
import tomllib
from pathlib import Path

from arxpm._toml import tomllib
from arxpm.errors import ManifestError
from arxpm.models import Manifest

Expand Down
2 changes: 1 addition & 1 deletion src/arxpm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class BuildConfig:
type: str
"""

entry: str = "src/main.arx"
entry: str = "src/main.x"
out_dir: str = "build"

def __post_init__(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/arxpm/pixi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
import json
import re
import shutil
import tomllib
from collections.abc import Callable, Iterable, Mapping
from pathlib import Path
from typing import Any

from arxpm._toml import tomllib
from arxpm.errors import ManifestError, MissingPixiError
from arxpm.external import CommandResult, CommandRunner, run_command

Expand Down
14 changes: 13 additions & 1 deletion src/arxpm/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,19 @@
from arxpm.models import DependencySpec, Manifest
from arxpm.pixi import PixiService

_MAIN_SOURCE = 'fn main() {\n print("Hello, Arx!");\n}\n'
_MAIN_SOURCE = """```
title: Simple main module
```

fn main() -> i32:
```
title: Print hello world
returns:
type: i32
```
print("Hello, Arx!")
return 0
"""


class ProjectPixiAdapter(Protocol):
Expand Down
32 changes: 30 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pytest
from typer.testing import CliRunner

from arxpm._toml import tomllib
from arxpm.cli import app
from arxpm.doctor import DoctorCheck, DoctorReport
from arxpm.errors import MissingPixiError
Expand All @@ -34,6 +35,15 @@ def run(self, directory: Path) -> DoctorReport:
return DoctorReport(checks=(DoctorCheck("pixi", True, "ok"),))


class PassingRunProjectService:
"""
title: Project service that always succeeds on run.
"""

def run(self, directory: Path) -> None:
return None


def test_init_command_creates_project_files(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
Expand All @@ -43,8 +53,13 @@ def test_init_command_creates_project_files(
result = runner.invoke(app, ["init", "--name", "hello-arx", "--no-pixi"])

assert result.exit_code == 0
assert (tmp_path / "arxproj.toml").exists()
assert (tmp_path / "src" / "main.arx").exists()
manifest_path = tmp_path / "arxproj.toml"
assert manifest_path.exists()

manifest_data = tomllib.loads(manifest_path.read_text(encoding="utf-8"))
entry = manifest_data["build"]["entry"]
assert isinstance(entry, str)
assert (tmp_path / entry).exists()


def test_add_command_writes_registry_dependency(
Expand Down Expand Up @@ -85,6 +100,19 @@ def test_install_command_requires_manifest(
assert "manifest not found" in result.output


def test_run_command_omits_completion_message(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"arxpm.cli.ProjectService",
PassingRunProjectService,
)

result = runner.invoke(app, ["run"])

assert result.exit_code == 0


def test_doctor_command_reports_success(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down
59 changes: 59 additions & 0 deletions tests/test_external.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
title: Tests for external command helpers.
"""

from __future__ import annotations

import sys
from pathlib import Path

import pytest

from arxpm.errors import ExternalCommandError
from arxpm.external import run_command


def test_run_command_forwards_stdout_and_stderr(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
result = run_command(
[
sys.executable,
"-c",
(
"import sys; "
"print('hello from stdout'); "
"print('hello from stderr', file=sys.stderr)"
),
],
cwd=tmp_path,
)

captured = capsys.readouterr()

assert result.returncode == 0
assert result.stdout == "hello from stdout\n"
assert result.stderr == "hello from stderr\n"
assert captured.out == "hello from stdout\n"
assert captured.err == "hello from stderr\n"


def test_run_command_check_raises_external_error() -> None:
with pytest.raises(ExternalCommandError) as exc_info:
run_command(
[
sys.executable,
"-c",
(
"import sys; "
"print('fatal', file=sys.stderr); "
"raise SystemExit(3)"
),
],
check=True,
)

error = exc_info.value
assert error.returncode == 3
assert "fatal" in error.stderr
Loading
Loading