From 7d4f60b9757a6f9e242b41e4c97809df7066c5a7 Mon Sep 17 00:00:00 2001 From: Ivan Ogasawara Date: Thu, 12 Mar 2026 03:33:38 +0000 Subject: [PATCH 1/4] feat: Add support for publishing with arxpm publish --- README.md | 4 +- docs/commands.md | 20 +++ docs/getting-started.md | 10 ++ docs/index.md | 3 +- docs/manifest.md | 4 +- src/arxpm/cli.py | 67 +++++++++ src/arxpm/project.py | 324 +++++++++++++++++++++++++++++++++++++++- tests/test_cli.py | 50 +++++++ tests/test_project.py | 112 ++++++++++++++ 9 files changed, 590 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 73af768..7ea2f52 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ workspace lifecycle, Pixi integration, and user-facing workflow commands. - `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`). +- `project.py`: project workflows (`init`, `add`, `install`, `build`, `run`, + `publish`). - `doctor.py`: health checks for environment and manifest. - `cli.py`: Typer command layer. @@ -28,6 +29,7 @@ workspace lifecycle, Pixi integration, and user-facing workflow commands. - `arxpm add [--path PATH|--git URL]` - `arxpm build` - `arxpm run` +- `arxpm publish` - `arxpm doctor` ## Development diff --git a/docs/commands.md b/docs/commands.md index 1502b57..ad86a4c 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -34,6 +34,12 @@ arxpm install arxpm install --directory examples ``` +Dependency entries are installed with pip inside the project pixi env: + +- registry: `pip install ` +- path: `pip install ` +- git: `pip install git+` + ## `arxpm build` Compile through Pixi using the configured compiler. @@ -59,6 +65,20 @@ arxpm run --directory examples ``` Build/compiler output and the application stdout/stderr are streamed directly; +`arxpm run` does not print an extra completion line. + +## `arxpm publish` + +Build and publish the current project as a Python package that bundles +`arxproj.toml` and `*.x`/`*.arx` sources. + +```bash +export TWINE_USERNAME=__token__ +export TWINE_PASSWORD= +arxpm publish +arxpm publish --repository-url https://test.pypi.org/legacy/ +arxpm publish --dry-run +``` ## `arxpm doctor` diff --git a/docs/getting-started.md b/docs/getting-started.md index ae01ef6..1f32cc4 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -40,6 +40,16 @@ python -m arxpm build --directory examples python -m arxpm run --directory examples ``` +## Publish Package + +Set Twine credentials for your package index (PyPI example): + +```bash +export TWINE_USERNAME=__token__ +export TWINE_PASSWORD= +python -m arxpm publish --directory examples +``` + ## Local Quality Gates ```bash diff --git a/docs/index.md b/docs/index.md index efce0b4..58bae06 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,7 +23,7 @@ only for distributing `arxpm` itself. - `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/project.py`: `init`, `add`, `install`, `build`, `run`, `publish`. - `src/arxpm/doctor.py`: environment and manifest checks. - `src/arxpm/cli.py`: Typer CLI layer. @@ -35,6 +35,7 @@ arxpm add http arxpm install arxpm build arxpm run +arxpm publish arxpm doctor ``` diff --git a/docs/manifest.md b/docs/manifest.md index c5a9c01..da9d059 100644 --- a/docs/manifest.md +++ b/docs/manifest.md @@ -43,4 +43,6 @@ mylib = { path = "../mylib" } utils = { git = "https://example.com/utils.git" } ``` -Version solving and registry resolution are intentionally out of scope in v0. +Version solving is intentionally out of scope in v0. During `arxpm install`, +registry dependencies are installed with `pip install `, path dependencies +with `pip install `, and git dependencies with `pip install git+`. diff --git a/src/arxpm/cli.py b/src/arxpm/cli.py index f7601ff..c34f5b5 100644 --- a/src/arxpm/cli.py +++ b/src/arxpm/cli.py @@ -187,6 +187,73 @@ def run_command( _fail(exc) +@app.command("publish") +def publish_command( + repository_url: Annotated[ + str | None, + typer.Option( + "--repository-url", + help="Override Python package repository upload URL.", + ), + ] = None, + skip_existing: Annotated[ + bool, + typer.Option( + "--skip-existing/--no-skip-existing", + help="Skip artifacts that already exist remotely.", + ), + ] = False, + dry_run: Annotated[ + bool, + typer.Option( + "--dry-run", + help="Build publish artifacts without uploading.", + ), + ] = False, + directory: Annotated[ + Path, + typer.Option("--directory", "-C", help="Project directory."), + ] = Path("."), +) -> None: + """ + title: Build and publish package artifacts to a PyPI-compatible index. + parameters: + repository_url: + type: >- + Annotated[str | None, typer.Option('--repository-url', help='Override + Python package repository upload URL.')] + skip_existing: + type: >- + Annotated[bool, typer.Option('--skip-existing/--no-skip-existing', + help='Skip artifacts that already exist remotely.')] + dry_run: + type: >- + Annotated[bool, typer.Option('--dry-run', help='Build publish + artifacts without uploading.')] + directory: + type: >- + Annotated[Path, typer.Option('--directory', '-C', help='Project + directory.')] + """ + project_service = ProjectService() + try: + result = project_service.publish( + _resolve(directory), + repository_url=repository_url, + skip_existing=skip_existing, + dry_run=dry_run, + ) + except ArxpmError as exc: + _fail(exc) + + artifacts = ", ".join(str(path) for path in result.artifacts) + if dry_run: + typer.echo(f"Publish dry-run completed. Artifacts: {artifacts}") + return + + typer.echo(f"Published artifacts: {artifacts}") + + @app.command() def doctor( directory: Annotated[ diff --git a/src/arxpm/project.py b/src/arxpm/project.py index 6bf991f..83d847f 100644 --- a/src/arxpm/project.py +++ b/src/arxpm/project.py @@ -4,6 +4,10 @@ from __future__ import annotations +import json +import re +import shutil +import tempfile from dataclasses import dataclass from pathlib import Path from typing import Protocol @@ -32,6 +36,20 @@ return 0 """ +_SOURCE_SUFFIXES = (".x", ".arx") +_EXCLUDED_SOURCE_DIRS = { + ".git", + ".mypy_cache", + ".pixi", + ".pytest_cache", + ".ruff_cache", + ".venv", + "__pycache__", + "build", + "dist", + "venv", +} + class ProjectPixiAdapter(Protocol): """ @@ -118,6 +136,24 @@ class RunResult: command_result: CommandResult +@dataclass(slots=True, frozen=True) +class PublishResult: + """ + title: Publish execution output. + attributes: + manifest: + type: Manifest + artifacts: + type: tuple[Path, Ellipsis] + upload_result: + type: CommandResult | None + """ + + manifest: Manifest + artifacts: tuple[Path, ...] + upload_result: CommandResult | None + + class ProjectService: """ title: High-level project workflows. @@ -227,7 +263,9 @@ def install(self, directory: Path) -> CommandResult: manifest.project.name, required_dependencies=_required_pixi_dependencies(), ) - return self._pixi.install(directory) + command_result = self._pixi.install(directory) + self._install_manifest_dependencies(directory, manifest) + return command_result def build(self, directory: Path) -> BuildResult: """ @@ -285,6 +323,290 @@ def run(self, directory: Path) -> RunResult: command_result=command_result, ) + def publish( + self, + directory: Path, + repository_url: str | None = None, + skip_existing: bool = False, + dry_run: bool = False, + ) -> PublishResult: + """ + title: Build and publish project sources as a Python package. + parameters: + directory: + type: Path + repository_url: + type: str | None + skip_existing: + type: bool + dry_run: + type: bool + returns: + type: PublishResult + """ + manifest = load_manifest(directory) + self._pixi.ensure_available() + self._pixi.ensure_manifest( + directory, + manifest.project.name, + required_dependencies=_required_pixi_dependencies(), + ) + self._pixi.install(directory) + + if repository_url is not None and not repository_url.strip(): + raise ManifestError("repository URL cannot be empty") + + # Ensure build/upload tooling is present in the active pixi env. + self._pixi.run( + directory, + [ + "python", + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--quiet", + "build", + "twine", + ], + ) + + dist_dir = directory / "dist" + dist_dir.mkdir(parents=True, exist_ok=True) + artifacts_before = { + path.resolve() for path in dist_dir.iterdir() if path.is_file() + } + + with tempfile.TemporaryDirectory(prefix="arxpm-publish-") as temp_dir: + staging_dir = Path(temp_dir) / "package" + _prepare_publish_workspace(directory, manifest, staging_dir) + self._pixi.run( + directory, + [ + "python", + "-m", + "build", + "--sdist", + "--wheel", + "--outdir", + str(dist_dir), + str(staging_dir), + ], + ) + + artifacts = tuple( + sorted( + ( + path + for path in dist_dir.iterdir() + if path.is_file() + and path.resolve() not in artifacts_before + ), + key=lambda path: path.name, + ) + ) + if not artifacts: + raise ManifestError("publish build produced no artifacts") + + if dry_run: + return PublishResult( + manifest=manifest, + artifacts=artifacts, + upload_result=None, + ) + + upload_cmd = [ + "python", + "-m", + "twine", + "upload", + "--non-interactive", + ] + if repository_url: + upload_cmd.extend(["--repository-url", repository_url.strip()]) + if skip_existing: + upload_cmd.append("--skip-existing") + upload_cmd.extend(str(path) for path in artifacts) + + upload_result = self._pixi.run(directory, upload_cmd) + return PublishResult( + manifest=manifest, + artifacts=artifacts, + upload_result=upload_result, + ) + + def _install_manifest_dependencies( + self, + directory: Path, + manifest: Manifest, + ) -> None: + for dependency_name, spec in sorted(manifest.dependencies.items()): + target = _dependency_install_target(dependency_name, spec) + self._pixi.run( + directory, + [ + "python", + "-m", + "pip", + "install", + "--disable-pip-version-check", + target, + ], + ) + def _required_pixi_dependencies() -> tuple[str, ...]: return ("clang", "python") + + +def _dependency_install_target(name: str, spec: DependencySpec) -> str: + if spec.source is not None: + return name + if spec.path is not None: + return spec.path + if spec.git is not None: + if spec.git.startswith("git+"): + return spec.git + return f"git+{spec.git}" + raise ManifestError(f"dependency {name!r} has no installable target") + + +def _prepare_publish_workspace( + directory: Path, + manifest: Manifest, + staging_dir: Path, +) -> None: + package_name = _distribution_to_package_name(manifest.project.name) + package_root = staging_dir / "src" / package_name + package_root.mkdir(parents=True, exist_ok=True) + + source_paths = _discover_arx_sources(directory) + if not source_paths: + raise ManifestError( + "no Arx source files found to publish (expected .x or .arx)" + ) + + for relative_source in source_paths: + source_path = directory / relative_source + target_path = package_root / relative_source + target_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, target_path) + + manifest_path = directory / "arxproj.toml" + if not manifest_path.exists(): + raise ManifestError(f"manifest not found: {manifest_path}") + + shutil.copy2(manifest_path, package_root / "arxproj.toml") + (package_root / "__init__.py").write_text( + _render_package_init(manifest), + encoding="utf-8", + ) + + staging_dir.mkdir(parents=True, exist_ok=True) + (staging_dir / "pyproject.toml").write_text( + _render_publish_pyproject(manifest, package_name), + encoding="utf-8", + ) + (staging_dir / "README.md").write_text( + _render_publish_readme(manifest), + encoding="utf-8", + ) + + +def _discover_arx_sources(directory: Path) -> list[Path]: + sources: list[Path] = [] + for path in directory.rglob("*"): + if not path.is_file() or path.suffix not in _SOURCE_SUFFIXES: + continue + + relative = path.relative_to(directory) + if any(part in _EXCLUDED_SOURCE_DIRS for part in relative.parts): + continue + sources.append(relative) + + return sorted(sources) + + +def _distribution_to_package_name(project_name: str) -> str: + cleaned = re.sub(r"[^A-Za-z0-9]+", "_", project_name).strip("_") + if not cleaned: + raise ManifestError("project.name must contain letters or numbers") + + if cleaned[0].isdigit(): + cleaned = f"pkg_{cleaned}" + return f"arxpkg_{cleaned.lower()}" + + +def _render_package_init(manifest: Manifest) -> str: + name = _toml_quote(manifest.project.name) + version = _toml_quote(manifest.project.version) + return ( + "\n".join( + [ + '"""Generated Arx package metadata."""', + "", + f"PROJECT_NAME = {name}", + f"PROJECT_VERSION = {version}", + "", + '__all__ = ["PROJECT_NAME", "PROJECT_VERSION"]', + ] + ) + + "\n" + ) + + +def _render_publish_pyproject( + manifest: Manifest, + package_name: str, +) -> str: + description = f"Published Arx package for project {manifest.project.name}." + lines = [ + "[build-system]", + 'requires = ["hatchling>=1.25.0"]', + 'build-backend = "hatchling.build"', + "", + "[project]", + f"name = {_toml_quote(manifest.project.name)}", + f"version = {_toml_quote(manifest.project.version)}", + f"description = {_toml_quote(description)}", + 'readme = "README.md"', + 'requires-python = ">=3.10"', + "", + "[tool.hatch.build.targets.wheel]", + f'packages = ["src/{package_name}"]', + "include = [", + f' "src/{package_name}/**/*.x",', + f' "src/{package_name}/**/*.arx",', + f' "src/{package_name}/arxproj.toml",', + "]", + "", + "[tool.hatch.build.targets.sdist]", + "include = [", + f' "src/{package_name}/**/*",', + ' "README.md",', + ' "pyproject.toml",', + "]", + ] + return "\n".join(lines) + "\n" + + +def _render_publish_readme(manifest: Manifest) -> str: + return ( + "\n".join( + [ + f"# {manifest.project.name}", + "", + "This package was generated by arxpm publish.", + "", + "It includes:", + "", + "- arxproj.toml", + "- Arx source files (*.x, *.arx)", + ] + ) + + "\n" + ) + + +def _toml_quote(value: str) -> str: + return json.dumps(value, ensure_ascii=True) diff --git a/tests/test_cli.py b/tests/test_cli.py index 7575d4e..e629c1c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,6 +5,7 @@ from __future__ import annotations from pathlib import Path +from types import SimpleNamespace import pytest from typer.testing import CliRunner @@ -44,6 +45,29 @@ def run(self, directory: Path) -> None: return None +class PassingPublishProjectService: + """ + title: Project service that always succeeds on publish. + """ + + def publish( + self, + directory: Path, + repository_url: str | None = None, + skip_existing: bool = False, + dry_run: bool = False, + ) -> SimpleNamespace: + _ = repository_url + _ = skip_existing + _ = dry_run + return SimpleNamespace( + artifacts=( + directory / "dist" / "demo-0.1.0-py3-none-any.whl", + directory / "dist" / "demo-0.1.0.tar.gz", + ) + ) + + def test_init_command_creates_project_files( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -113,6 +137,32 @@ def test_run_command_omits_completion_message( assert result.exit_code == 0 +def test_publish_command_reports_artifacts( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.setattr( + "arxpm.cli.ProjectService", + PassingPublishProjectService, + ) + + result = runner.invoke( + app, + [ + "publish", + "--dry-run", + "--repository-url", + "https://test.pypi.org/legacy/", + "--skip-existing", + ], + ) + + assert result.exit_code == 0 + assert "Publish dry-run completed." in result.output + assert "demo-0.1.0-py3-none-any.whl" in result.output + + def test_doctor_command_reports_success( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/test_project.py b/tests/test_project.py index 5f0e2b6..ce1384c 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -53,6 +53,27 @@ def install(self, directory: Path) -> CommandResult: def run(self, directory: Path, args: list[str]) -> CommandResult: self.run_calls.append((directory, args)) + + if args[:3] == ["python", "-m", "build"] and "--outdir" in args: + outdir_value = Path(args[args.index("--outdir") + 1]) + if outdir_value.is_absolute(): + outdir = outdir_value + else: + outdir = directory / outdir_value + outdir.mkdir(parents=True, exist_ok=True) + + manifest = load_manifest(directory) + normalized = manifest.project.name.replace("-", "_") + version = manifest.project.version + (outdir / f"{normalized}-{version}.tar.gz").write_text( + "", + encoding="utf-8", + ) + (outdir / f"{normalized}-{version}-py3-none-any.whl").write_text( + "", + encoding="utf-8", + ) + return CommandResult(("pixi", "run", *args), 0, "", "") @@ -105,6 +126,97 @@ def test_install_calls_pixi_install(tmp_path: Path) -> None: assert pixi.ensure_manifest_calls +def test_install_installs_manifest_dependencies_with_pip( + tmp_path: Path, +) -> None: + pixi = FakePixiService() + service = ProjectService(pixi=pixi) + service.init(tmp_path, name="demo", create_pixi=False) + + service.add_dependency(tmp_path, "http") + service.add_dependency(tmp_path, "mylib", path=Path("../mylib")) + service.add_dependency( + tmp_path, + "utils", + git="https://example.com/utils.git", + ) + + service.install(tmp_path) + + commands = [call[1] for call in pixi.run_calls] + assert commands == [ + [ + "python", + "-m", + "pip", + "install", + "--disable-pip-version-check", + "http", + ], + [ + "python", + "-m", + "pip", + "install", + "--disable-pip-version-check", + "../mylib", + ], + [ + "python", + "-m", + "pip", + "install", + "--disable-pip-version-check", + "git+https://example.com/utils.git", + ], + ] + + +def test_publish_builds_and_uploads_artifacts(tmp_path: Path) -> None: + pixi = FakePixiService() + service = ProjectService(pixi=pixi) + service.init(tmp_path, name="demo", create_pixi=False) + + publish_result = service.publish( + tmp_path, + repository_url="https://test.pypi.org/legacy/", + skip_existing=True, + ) + + assert [path.name for path in publish_result.artifacts] == [ + "demo-0.1.0-py3-none-any.whl", + "demo-0.1.0.tar.gz", + ] + assert publish_result.upload_result is not None + assert pixi.install_calls == [tmp_path] + + commands = [call[1] for call in pixi.run_calls] + assert commands[0][:5] == [ + "python", + "-m", + "pip", + "install", + "--disable-pip-version-check", + ] + assert commands[1][:3] == ["python", "-m", "build"] + + upload_command = commands[2] + assert upload_command[:4] == ["python", "-m", "twine", "upload"] + assert "--repository-url" in upload_command + assert "--skip-existing" in upload_command + + +def test_publish_dry_run_skips_upload(tmp_path: Path) -> None: + pixi = FakePixiService() + service = ProjectService(pixi=pixi) + service.init(tmp_path, name="demo", create_pixi=False) + + publish_result = service.publish(tmp_path, dry_run=True) + + assert publish_result.upload_result is None + assert len(pixi.run_calls) == 2 + + def test_install_requires_arxproj_manifest(tmp_path: Path) -> None: pixi = FakePixiService() service = ProjectService(pixi=pixi) From 7691674908f387006b0621b61b7b40fdcb086553 Mon Sep 17 00:00:00 2001 From: Ivan Ogasawara Date: Thu, 12 Mar 2026 03:39:32 +0000 Subject: [PATCH 2/4] add publish command --- README.md | 4 +- docs/commands.md | 19 ++ docs/index.md | 5 +- poetry.lock | 391 ++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + src/arxpm/cli.py | 59 ++++++- src/arxpm/project.py | 11 ++ tests/test_cli.py | 52 +++++- tests/test_project.py | 15 ++ 9 files changed, 537 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 7ea2f52..791a029 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ workspace lifecycle, Pixi integration, and user-facing workflow commands. - `_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`, - `publish`). + `pack`, `publish`). - `doctor.py`: health checks for environment and manifest. - `cli.py`: Typer command layer. @@ -28,7 +28,9 @@ workspace lifecycle, Pixi integration, and user-facing workflow commands. - `arxpm install` - `arxpm add [--path PATH|--git URL]` - `arxpm build` +- `arxpm compile` - `arxpm run` +- `arxpm pack` - `arxpm publish` - `arxpm doctor` diff --git a/docs/commands.md b/docs/commands.md index ad86a4c..ebf3ab7 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -55,6 +55,16 @@ Current Arx invocation uses: arx --output-file / ``` +## `arxpm compile` + +Compile through Pixi using the configured compiler. This is a clearer alias for +`arxpm build` (which is kept for compatibility). + +```bash +arxpm compile +arxpm compile --directory examples +``` + ## `arxpm run` Build and then run the produced artifact through Pixi. @@ -67,6 +77,15 @@ arxpm run --directory examples Build/compiler output and the application stdout/stderr are streamed directly; `arxpm run` does not print an extra completion line. +## `arxpm pack` + +Build package artifacts locally without uploading to a registry. + +```bash +arxpm pack +arxpm pack --directory examples +``` + ## `arxpm publish` Build and publish the current project as a Python package that bundles diff --git a/docs/index.md b/docs/index.md index 58bae06..933a086 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,7 +23,8 @@ only for distributing `arxpm` itself. - `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`, `publish`. +- `src/arxpm/project.py`: `init`, `add`, `install`, `build`, `run`, `pack`, + `publish`. - `src/arxpm/doctor.py`: environment and manifest checks. - `src/arxpm/cli.py`: Typer CLI layer. @@ -34,7 +35,9 @@ arxpm init --name hello-arx arxpm add http arxpm install arxpm build +arxpm compile arxpm run +arxpm pack arxpm publish arxpm doctor ``` diff --git a/poetry.lock b/poetry.lock index fd0c9a3..53049bd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -261,6 +261,23 @@ files = [ [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + [[package]] name = "backrefs" version = "6.2" @@ -451,7 +468,7 @@ version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, @@ -463,7 +480,7 @@ version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, @@ -550,6 +567,7 @@ files = [ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] +markers = {main = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} @@ -572,7 +590,7 @@ version = "3.4.5" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765"}, {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990"}, @@ -860,7 +878,7 @@ version = "46.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}, {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}, @@ -912,6 +930,7 @@ files = [ {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}, {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}, ] +markers = {main = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\""} [package.dependencies] cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} @@ -1003,6 +1022,18 @@ files = [ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] +[[package]] +name = "docutils" +version = "0.22.4" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"}, + {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}, +] + [[package]] name = "douki" version = "0.11.0" @@ -1314,6 +1345,26 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "id" +version = "1.6.1" +description = "A tool for generating OIDC identities" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca"}, + {file = "id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069"}, +] + +[package.dependencies] +urllib3 = ">=2,<3" + +[package.extras] +dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] +lint = ["bandit", "interrogate", "mypy", "ruff (<0.14.15)"] +test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] + [[package]] name = "identify" version = "2.6.17" @@ -1335,7 +1386,7 @@ version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, @@ -1344,6 +1395,31 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" +files = [ + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + [[package]] name = "iniconfig" version = "2.3.0" @@ -1506,6 +1582,74 @@ files = [ [package.dependencies] arrow = ">=0.15.0" +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.1.1" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco_context-6.1.1-py3-none-any.whl", hash = "sha256:0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808"}, + {file = "jaraco_context-6.1.1.tar.gz", hash = "sha256:bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.test (>=5.6.0)", "portend", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176"}, + {file = "jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb"}, +] + +[package.dependencies] +more_itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + [[package]] name = "jedi" version = "0.19.2" @@ -1526,6 +1670,23 @@ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alab qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] +[[package]] +name = "jeepney" +version = "0.9.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" +files = [ + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, +] + +[package.extras] +test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["trio"] + [[package]] name = "jinja2" version = "3.1.6" @@ -1860,6 +2021,37 @@ test-functional = ["black", "pytest", "pytest-asyncio", "pytest-randomly", "pyte test-integration = ["black", "ipykernel", "jupyter-server (!=2.11)", "nbconvert", "pytest", "pytest-asyncio", "pytest-randomly", "pytest-xdist"] test-ui = ["bash-kernel"] +[[package]] +name = "keyring" +version = "25.7.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, + {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, +] + +[package.dependencies] +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] + [[package]] name = "lark" version = "1.3.1" @@ -2606,6 +2798,19 @@ mkdocs-autorefs = ">=1.4" mkdocstrings = ">=0.30" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} +[[package]] +name = "more-itertools" +version = "10.8.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b"}, + {file = "more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd"}, +] + [[package]] name = "mypy" version = "1.19.1" @@ -2793,6 +2998,43 @@ files = [ {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, ] +[[package]] +name = "nh3" +version = "0.3.3" +description = "Python binding to Ammonia HTML sanitizer Rust crate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "nh3-0.3.3-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:21b058cd20d9f0919421a820a2843fdb5e1749c0bf57a6247ab8f4ba6723c9fc"}, + {file = "nh3-0.3.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4400a73c2a62859e769f9d36d1b5a7a5c65c4179d1dddd2f6f3095b2db0cbfc"}, + {file = "nh3-0.3.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ef87f8e916321a88b45f2d597f29bd56e560ed4568a50f0f1305afab86b7189"}, + {file = "nh3-0.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a446eae598987f49ee97ac2f18eafcce4e62e7574bd1eb23782e4702e54e217d"}, + {file = "nh3-0.3.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0d5eb734a78ac364af1797fef718340a373f626a9ff6b4fb0b4badf7927e7b81"}, + {file = "nh3-0.3.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92a958e6f6d0100e025a5686aafd67e3c98eac67495728f8bb64fbeb3e474493"}, + {file = "nh3-0.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9ed40cf8449a59a03aa465114fedce1ff7ac52561688811d047917cc878b19ca"}, + {file = "nh3-0.3.3-cp314-cp314t-win32.whl", hash = "sha256:b50c3770299fb2a7c1113751501e8878d525d15160a4c05194d7fe62b758aad8"}, + {file = "nh3-0.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:21a63ccb18ddad3f784bb775955839b8b80e347e597726f01e43ca1abcc5c808"}, + {file = "nh3-0.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f508ddd4e2433fdcb78c790fc2d24e3a349ba775e5fa904af89891321d4844a3"}, + {file = "nh3-0.3.3-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e8ee96156f7dfc6e30ecda650e480c5ae0a7d38f0c6fafc3c1c655e2500421d9"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45fe0d6a607264910daec30360c8a3b5b1500fd832d21b2da608256287bcb92d"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bc1d4b30ba1ba896669d944b6003630592665974bd11a3dc2f661bde92798a7"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f433a2dd66545aad4a720ad1b2150edcdca75bfff6f4e6f378ade1ec138d5e77"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52e973cb742e95b9ae1b35822ce23992428750f4b46b619fe86eba4205255b30"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c730617bdc15d7092dcc0469dc2826b914c8f874996d105b4bc3842a41c1cd9"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e98fa3dbfd54e25487e36ba500bc29bca3a4cab4ffba18cfb1a35a2d02624297"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3a62b8ae7c235481715055222e54c682422d0495a5c73326807d4e44c5d14691"}, + {file = "nh3-0.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc305a2264868ec8fa16548296f803d8fd9c1fa66cd28b88b605b1bd06667c0b"}, + {file = "nh3-0.3.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90126a834c18af03bfd6ff9a027bfa6bbf0e238527bc780a24de6bd7cc1041e2"}, + {file = "nh3-0.3.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:24769a428e9e971e4ccfb24628f83aaa7dc3c8b41b130c8ddc1835fa1c924489"}, + {file = "nh3-0.3.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:b7a18ee057761e455d58b9d31445c3e4b2594cff4ddb84d2e331c011ef46f462"}, + {file = "nh3-0.3.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a4b2c1f3e6f3cbe7048e17f4fefad3f8d3e14cc0fd08fb8599e0d5653f6b181"}, + {file = "nh3-0.3.3-cp38-abi3-win32.whl", hash = "sha256:e974850b131fdffa75e7ad8e0d9c7a855b96227b093417fdf1bd61656e530f37"}, + {file = "nh3-0.3.3-cp38-abi3-win_amd64.whl", hash = "sha256:2efd17c0355d04d39e6d79122b42662277ac10a17ea48831d90b46e5ef7e4fc0"}, + {file = "nh3-0.3.3-cp38-abi3-win_arm64.whl", hash = "sha256:b838e619f483531483d26d889438e53a880510e832d2aafe73f93b7b1ac2bce2"}, + {file = "nh3-0.3.3.tar.gz", hash = "sha256:185ed41b88c910b9ca8edc89ca3b4be688a12cb9de129d84befa2f74a0039fee"}, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -2842,7 +3084,7 @@ version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, @@ -3124,12 +3366,12 @@ version = "3.0" description = "C parser in Python" optional = false python-versions = ">=3.10" -groups = ["dev"] -markers = "implementation_name != \"PyPy\"" +groups = ["main", "dev"] files = [ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] +markers = {main = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", dev = "implementation_name != \"PyPy\""} [[package]] name = "pygments" @@ -3351,6 +3593,19 @@ files = [ [package.dependencies] Levenshtein = "0.27.3" +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "pywinpty" version = "3.0.3" @@ -3677,6 +3932,26 @@ files = [ [package.extras] all = ["numpy"] +[[package]] +name = "readme-renderer" +version = "44.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, +] + +[package.dependencies] +docutils = ">=0.21.2" +nh3 = ">=0.2.14" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + [[package]] name = "referencing" version = "0.37.0" @@ -3700,7 +3975,7 @@ version = "2.32.5" description = "Python HTTP for Humans." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, @@ -3716,6 +3991,21 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + [[package]] name = "rfc3339-validator" version = "0.1.4" @@ -3731,6 +4021,21 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + [[package]] name = "rfc3986-validator" version = "0.1.1" @@ -3933,6 +4238,23 @@ files = [ {file = "ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2"}, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" +files = [ + {file = "secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}, + {file = "secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "send2trash" version = "2.1.0" @@ -4323,6 +4645,32 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] +[[package]] +name = "twine" +version = "6.2.0" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8"}, + {file = "twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf"}, +] + +[package.dependencies] +id = "*" +keyring = {version = ">=21.2.0", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} +packaging = ">=24.0" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + +[package.extras] +keyring = ["keyring (>=21.2.0)"] + [[package]] name = "typeguard" version = "4.5.1" @@ -4446,7 +4794,7 @@ version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, @@ -4659,7 +5007,28 @@ ptk = ["prompt-toolkit (>=3.0.29)", "pyperclip"] pygments = ["pygments (>=2.2)"] test = ["coverage (>=5.3.1)", "prompt-toolkit (>=3.0.29)", "pygments (>=2.2)", "pyte (>=0.8.0)", "pytest (>=7)", "pytest-cov", "pytest-mock", "pytest-rerunfailures", "pytest-subprocess", "pytest-timeout", "requests", "restructuredtext_lint", "virtualenv (>=20.16.2)", "xonsh[bestshell]"] +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [metadata] lock-version = "2.1" python-versions = ">=3.10,<4" -content-hash = "64abf142ee53fc45488c66dcd5f8456bd76070204114419900ad1cb7738512c3" +content-hash = "ff7895e13d84730128b5cd2c9476517cd23ecd8b4ebba7e594b8fc48d36e58d2" diff --git a/pyproject.toml b/pyproject.toml index ec139a1..738ae63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "typer>=0.12.3,<1.0.0", "arxlang >= 0.3.3", "tomli>=2.0.1; python_version < '3.11'", + "twine (>=6.2.0)", ] [project.scripts] diff --git a/src/arxpm/cli.py b/src/arxpm/cli.py index c34f5b5..274fb98 100644 --- a/src/arxpm/cli.py +++ b/src/arxpm/cli.py @@ -141,6 +141,16 @@ def add( typer.echo(f"Added dependency {name} ({kind}).") +def _compile_project(directory: Path, label: str) -> None: + project_service = ProjectService() + try: + result = project_service.build(_resolve(directory)) + except ArxpmError as exc: + _fail(exc) + + typer.echo(f"{label} completed. Artifact target: {result.artifact}") + + @app.command("build") def build_command( directory: Annotated[ @@ -156,13 +166,25 @@ def build_command( Annotated[Path, typer.Option('--directory', '-C', help='Project directory.')] """ - project_service = ProjectService() - try: - result = project_service.build(_resolve(directory)) - except ArxpmError as exc: - _fail(exc) + _compile_project(directory, "Build") + - typer.echo(f"Build completed. Artifact target: {result.artifact}") +@app.command("compile") +def compile_command( + directory: Annotated[ + Path, + typer.Option("--directory", "-C", help="Project directory."), + ] = Path("."), +) -> None: + """ + title: Compile project sources into a runnable binary artifact. + parameters: + directory: + type: >- + Annotated[Path, typer.Option('--directory', '-C', help='Project + directory.')] + """ + _compile_project(directory, "Compile") @app.command("run") @@ -187,6 +209,31 @@ def run_command( _fail(exc) +@app.command("pack") +def pack_command( + directory: Annotated[ + Path, + typer.Option("--directory", "-C", help="Project directory."), + ] = Path("."), +) -> None: + """ + title: Build package artifacts without uploading to an index. + parameters: + directory: + type: >- + Annotated[Path, typer.Option('--directory', '-C', help='Project + directory.')] + """ + project_service = ProjectService() + try: + result = project_service.pack(_resolve(directory)) + except ArxpmError as exc: + _fail(exc) + + artifacts = ", ".join(str(path) for path in result.artifacts) + typer.echo(f"Pack completed. Artifacts: {artifacts}") + + @app.command("publish") def publish_command( repository_url: Annotated[ diff --git a/src/arxpm/project.py b/src/arxpm/project.py index 83d847f..0aa003f 100644 --- a/src/arxpm/project.py +++ b/src/arxpm/project.py @@ -323,6 +323,17 @@ def run(self, directory: Path) -> RunResult: command_result=command_result, ) + def pack(self, directory: Path) -> PublishResult: + """ + title: Build package artifacts without uploading. + parameters: + directory: + type: Path + returns: + type: PublishResult + """ + return self.publish(directory, dry_run=True) + def publish( self, directory: Path, diff --git a/tests/test_cli.py b/tests/test_cli.py index e629c1c..39d8450 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -45,11 +45,28 @@ def run(self, directory: Path) -> None: return None +class PassingBuildProjectService: + """ + title: Project service that always succeeds on build. + """ + + def build(self, directory: Path) -> SimpleNamespace: + return SimpleNamespace(artifact=directory / "build" / "demo") + + class PassingPublishProjectService: """ - title: Project service that always succeeds on publish. + title: Project service that always succeeds on publish/pack. """ + def pack(self, directory: Path) -> SimpleNamespace: + return SimpleNamespace( + artifacts=( + directory / "dist" / "demo-0.1.0-py3-none-any.whl", + directory / "dist" / "demo-0.1.0.tar.gz", + ) + ) + def publish( self, directory: Path, @@ -124,6 +141,22 @@ def test_install_command_requires_manifest( assert "manifest not found" in result.output +def test_compile_command_reports_artifact( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.setattr( + "arxpm.cli.ProjectService", + PassingBuildProjectService, + ) + + result = runner.invoke(app, ["compile"]) + + assert result.exit_code == 0 + assert "Compile completed." in result.output + + def test_run_command_omits_completion_message( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -137,6 +170,23 @@ def test_run_command_omits_completion_message( assert result.exit_code == 0 +def test_pack_command_reports_artifacts( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.setattr( + "arxpm.cli.ProjectService", + PassingPublishProjectService, + ) + + result = runner.invoke(app, ["pack"]) + + assert result.exit_code == 0 + assert "Pack completed." in result.output + assert "demo-0.1.0-py3-none-any.whl" in result.output + + def test_publish_command_reports_artifacts( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_project.py b/tests/test_project.py index ce1384c..6899266 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -206,6 +206,21 @@ def test_publish_builds_and_uploads_artifacts(tmp_path: Path) -> None: assert "--skip-existing" in upload_command +def test_pack_builds_artifacts_without_upload(tmp_path: Path) -> None: + pixi = FakePixiService() + service = ProjectService(pixi=pixi) + service.init(tmp_path, name="demo", create_pixi=False) + + pack_result = service.pack(tmp_path) + + assert [path.name for path in pack_result.artifacts] == [ + "demo-0.1.0-py3-none-any.whl", + "demo-0.1.0.tar.gz", + ] + assert pack_result.upload_result is None + assert len(pixi.run_calls) == 2 + + def test_publish_dry_run_skips_upload(tmp_path: Path) -> None: pixi = FakePixiService() service = ProjectService(pixi=pixi) From 5e749c629ab4ac9a160b48c3c5deafa580823f9d Mon Sep 17 00:00:00 2001 From: Ivan Ogasawara Date: Thu, 12 Mar 2026 03:41:59 +0000 Subject: [PATCH 3/4] fix docs --- docs/commands.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index ebf3ab7..6dd98d5 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -74,8 +74,7 @@ arxpm run arxpm run --directory examples ``` -Build/compiler output and the application stdout/stderr are streamed directly; -`arxpm run` does not print an extra completion line. +The command shows compiler and program output directly in your terminal. ## `arxpm pack` From a249ee299bddcbb50f468bef6b0c3de94f465c43 Mon Sep 17 00:00:00 2001 From: Ivan Ogasawara Date: Thu, 12 Mar 2026 03:51:55 +0000 Subject: [PATCH 4/4] add missing pip --- docs/getting-started.md | 2 +- docs/pixi-integration.md | 2 +- examples/arxproj.toml | 3 --- examples/pixi.toml | 1 + poetry.lock | 14 +++++++++++++- pyproject.toml | 1 + src/arxpm/doctor.py | 31 ++++++++++++++++++++++++------- src/arxpm/pixi.py | 2 +- src/arxpm/project.py | 2 +- tests/test_doctor.py | 10 ++++++++-- 10 files changed, 51 insertions(+), 17 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 1f32cc4..55b87de 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -30,7 +30,7 @@ The report should show: - pixi available - `arxproj.toml` found - `pixi.toml` found -- `python` and `clang` declared in `pixi.toml` +- `python`, `pip`, and `clang` declared in `pixi.toml` ## Run Example diff --git a/docs/pixi-integration.md b/docs/pixi-integration.md index c1ef1f8..80c69d6 100644 --- a/docs/pixi-integration.md +++ b/docs/pixi-integration.md @@ -6,7 +6,7 @@ `arxpm` does not own all of `pixi.toml`. It only manages: -- required toolchain dependencies (`python`, `clang`) +- required toolchain dependencies (`python`, `pip`, `clang`) - the `tool.arxpm` section used to track managed fields Unrelated user sections such as tasks and features are preserved. diff --git a/examples/arxproj.toml b/examples/arxproj.toml index 6426203..3e0f26e 100644 --- a/examples/arxproj.toml +++ b/examples/arxproj.toml @@ -8,9 +8,6 @@ entry = "src/main.x" out_dir = "build" [dependencies] -http = { source = "registry" } -mylib = { path = "../mylib" } -utils = { git = "https://example.com/utils.git" } [toolchain] compiler = "arx" diff --git a/examples/pixi.toml b/examples/pixi.toml index 2ff71d4..fa60c90 100644 --- a/examples/pixi.toml +++ b/examples/pixi.toml @@ -6,4 +6,5 @@ platforms = ["linux-64", "osx-64", "win-64"] [dependencies] python = "*" +pip = "*" clang = "*" diff --git a/poetry.lock b/poetry.lock index 53049bd..c191cf3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3189,6 +3189,18 @@ files = [ [package.dependencies] ptyprocess = ">=0.5" +[[package]] +name = "pip" +version = "26.0.1" +description = "The PyPA recommended tool for installing Python packages." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b"}, + {file = "pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8"}, +] + [[package]] name = "platformdirs" version = "4.9.4" @@ -5031,4 +5043,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4" -content-hash = "ff7895e13d84730128b5cd2c9476517cd23ecd8b4ebba7e594b8fc48d36e58d2" +content-hash = "273b7f04fede0f1460030fa4d4d30753eb806aae405d4c90cd8768dcc701b447" diff --git a/pyproject.toml b/pyproject.toml index 738ae63..a302a73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "arxlang >= 0.3.3", "tomli>=2.0.1; python_version < '3.11'", "twine (>=6.2.0)", + "pip (>=26.0.1)", ] [project.scripts] diff --git a/src/arxpm/doctor.py b/src/arxpm/doctor.py index 1e5c1ab..a1c0c2c 100644 --- a/src/arxpm/doctor.py +++ b/src/arxpm/doctor.py @@ -145,9 +145,11 @@ def run(self, directory: Path) -> DoctorReport: ) ) - python_declared, clang_declared, detail = self._dependency_checks( - directory, - pixi_file_ok, + python_declared, pip_declared, clang_declared, detail = ( + self._dependency_checks( + directory, + pixi_file_ok, + ) ) checks.append( DoctorCheck( @@ -156,6 +158,13 @@ def run(self, directory: Path) -> DoctorReport: message=detail["python"], ) ) + checks.append( + DoctorCheck( + name="pip declared", + ok=pip_declared, + message=detail["pip"], + ) + ) checks.append( DoctorCheck( name="clang declared", @@ -170,24 +179,27 @@ def _dependency_checks( self, directory: Path, pixi_file_ok: bool, - ) -> tuple[bool, bool, dict[str, str]]: + ) -> tuple[bool, bool, bool, dict[str, str]]: if not pixi_file_ok: details = { "python": f"{PIXI_FILENAME} missing", + "pip": f"{PIXI_FILENAME} missing", "clang": f"{PIXI_FILENAME} missing", } - return (False, False, details) + return (False, False, False, details) try: dependencies = self._pixi.declared_dependencies(directory) except ManifestError as exc: details = { "python": str(exc), + "pip": str(exc), "clang": str(exc), } - return (False, False, details) + return (False, False, False, details) python_ok = "python" in dependencies + pip_ok = "pip" in dependencies clang_ok = "clang" in dependencies details = { "python": ( @@ -195,10 +207,15 @@ def _dependency_checks( if python_ok else "python is not declared in pixi.toml" ), + "pip": ( + "pip is declared in pixi.toml" + if pip_ok + else "pip is not declared in pixi.toml" + ), "clang": ( "clang is declared in pixi.toml" if clang_ok else "clang is not declared in pixi.toml" ), } - return (python_ok, clang_ok, details) + return (python_ok, pip_ok, clang_ok, details) diff --git a/src/arxpm/pixi.py b/src/arxpm/pixi.py index 32e5506..ff83ad7 100644 --- a/src/arxpm/pixi.py +++ b/src/arxpm/pixi.py @@ -18,7 +18,7 @@ PIXI_FILENAME = "pixi.toml" DEFAULT_CHANNELS = ("conda-forge",) DEFAULT_PLATFORMS = ("linux-64", "osx-64", "win-64") -BASE_DEPENDENCIES = ("python", "clang") +BASE_DEPENDENCIES = ("python", "pip", "clang") _SIMPLE_KEY_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$") diff --git a/src/arxpm/project.py b/src/arxpm/project.py index 0aa003f..2cd4472 100644 --- a/src/arxpm/project.py +++ b/src/arxpm/project.py @@ -467,7 +467,7 @@ def _install_manifest_dependencies( def _required_pixi_dependencies() -> tuple[str, ...]: - return ("clang", "python") + return ("clang", "pip", "python") def _dependency_install_target(name: str, spec: DependencySpec) -> str: diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 322f713..ce9275d 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -56,7 +56,7 @@ def test_doctor_reports_requested_health_checks(tmp_path: Path) -> None: service = DoctorService( pixi=FakePixiService( available=True, - dependencies={"python", "clang"}, + dependencies={"python", "pip", "clang"}, ), ) @@ -67,19 +67,24 @@ def test_doctor_reports_requested_health_checks(tmp_path: Path) -> None: assert checks["arxproj.toml"].ok is True assert checks["pixi.toml"].ok is True assert checks["python declared"].ok is True + assert checks["pip declared"].ok is True assert checks["clang declared"].ok is True def test_doctor_reports_missing_pixi_manifest(tmp_path: Path) -> None: save_manifest(tmp_path, create_default_manifest("demo")) service = DoctorService( - pixi=FakePixiService(available=True, dependencies={"python", "clang"}), + pixi=FakePixiService( + available=True, + dependencies={"python", "pip", "clang"}, + ), ) report = service.run(tmp_path) checks = {check.name: check for check in report.checks} assert checks["pixi.toml"].ok is False assert checks["python declared"].ok is False + assert checks["pip declared"].ok is False assert checks["clang declared"].ok is False @@ -93,4 +98,5 @@ def test_doctor_reports_invalid_pixi_file(tmp_path: Path) -> None: report = service.run(tmp_path) checks = {check.name: check for check in report.checks} assert checks["python declared"].ok is False + assert checks["pip declared"].ok is False assert checks["clang declared"].ok is False