diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fa28fbc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: test + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + env: + UV_LOCKED: "true" + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Lint + run: | + uv run pre-commit run --all --show-diff-on-failure + + - name: Test + run: | + uv run python -m pytest --color=yes + + - name: Check package + shell: bash + run: | + uv build + uvx --with `find dist/*.whl` pixi-devenv --help diff --git a/.gitignore b/.gitignore index b7faf40..db3e87e 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,4 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ +/.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..deac87e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: +- repo: local + hooks: + - id: ruff-format + name: ruff-format + entry: uv run ruff format --force-exclude + pass_filenames: true + language: system + types_or: [ python, pyi ] + require_serial: true + stages: [ pre-commit ] + - id: ruff-check + name: ruff-check + entry: uv run ruff check --fix + language: system + types_or: [ python ] + pass_filenames: true + require_serial: false + stages: [ pre-commit ] + - id: mypy + name: mypy + entry: uv run mypy + types_or: [ python, pyi ] + pass_filenames: false + language: system + require_serial: true + stages: [ pre-commit ] + diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/README.md b/README.md index a93541b..30c4fdc 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,267 @@ # pixi-devenv + +## Why? + pixi-devenv is tool to work with multiple pixi projects in development mode. -**WORK IN PROGRESS** +pixi currently does not have full support to work with multiple projects in development mode. Development mode allows one to use have each project declaring its own dependencies, and work with all of them with live code so changes are reflected immediately, without the needing of creating/installing the projects as packages in the environment. + +pixi-devenv makes it easy to aggregate multiple "projects" to create a single "product". + +## Introduction -## `pixi.devenv.toml` +Here are a quick explanation of some `pixi` concepts that are important to understand to use `pixi-devenv`. -Environment configuration is placed in `pixi.devenv.toml` files, next to their usual `pixi.toml` files. +### `[dependencies]` + +Lists the `conda` dependencies of a project. `[pypi-dependencies]` lists PyPI dependencies. `pixi` fully supports using PyPI packages, meaning PyPI packages are solved together with the conda packages. -A `pixi.devenv.toml` file *includes* declarations from other projects by using the `includes` property: ```toml -includes = [ - "../core", - "../calc", +[dependencies] +alive-progress = ">=3.2" +artifacts_cache = ">=3.0" +``` + +### `[activation]` + +Defines which variables and scripts should be activated for the environment. + +```toml +[activation] +scripts = [ + ".pixi-activation/env-vars.sh", ] +CONDA_PY="310" +PATH = "$PIXI_PROJECT_ROOT/bin:$PATH" +``` + +### `[target.{NAME}]` + + +A `[target.{NAME}]` section can be used to specify platform specific configuration, such as `[target.win]` or `[target.linux]`. Generic terms are valid (`win`, `unix`), down to more specific ones (`linux-64`, `windows-64`). + +Each `[target.{NAME}]` section contains its own `[dependencies]` and `[activation]` sections. + +```toml +[target.win.dependencies] +pywin32 = ">=305" + +[target.unix.dependencies] +sqlite = ">=3.40" + +[target.linux-64.activation] +env = { JOBS = "6" } +``` + + +### `[feature.NAME]` + +Think of a `feature` as a group of `dependencies` and `activation` sections. They can be used to have a different set of dependencies for different purposes, like testing or linting tools, as well as different dependency matrixes. They are *additive* to the default `[environment]` and `[activation]` sections: + +```toml +[feature.python310] +dependencies = { python = "3.10.*" } +activation = { env = { CONDA_PY = "310" } } + +[feature.compile.target.win.dependencies] +dependency-walker = "*" +``` + +### `[environments]` + +Environments are sets of one or more features. An environment will contain all the `dependencies` and `activation` of the features that compose the environment. + +```toml +[environment] +py310 = ["python310", "compile"] +py312 = ["python312", "compile"] +``` + +## pixi-devenv + +`pixi-devenv` configuration resides in a `pixi.devenv.toml` file. To update `pixi.toml` in case `pixi.devenv.toml` changes, execute: + +```console +pixi run pixi-devenv +``` + +If your project includes `pixi-devenv` in its `dependencies`, but it can be run from a one-off environment: + +```console +pixi exec pixi-devenv +``` + +Consider this project structure: + +``` +workspace/ + core/ + src/ + pixi.devenv.toml + calc/ + src/ + pixi.devenv.toml + web/ + src/ + pixi.devenv.toml +``` + +**Characteristics** + +* `web` depends on `calc`, which depends on `core`. +* We have two features defined in `core`: + * `test`: adds test specific dependencies. + * `py310`: Python 3.10. + * `py312`: Python 3.12. + +### `core/pixi.devenv.toml` + +The `pixi-devenv` configuration resides in the `devenv` table. This avoids confusion when looking at both `pixi.devenv.toml` file and `pixi.toml`, making the distintion clear. + +```toml +[devenv] +# Mandatory: name of this project +# Question: should this actually be forbidden and forced to be the name of the directory? +name = "core" +channels = [ + "prefix.dev", + "https://packages.company.com" +] +platforms = ["win-64", "linux-64"] +``` + +Basic information about the project. `channels` and `platforms` are inherited by downstream projects by default, but can also be overwritten. + + +```toml +[devenv.dependencies] +attrs = "*" +boltons = "*" + +[devenv.target.win.dependencies] +pywin32 = "*" +``` + +Default dependencies, identical to pixi's `[dependencies]` section. They are inherited by default by downstream projects. + + +```toml +[devenv.constraints] +qt = ">=5.15" + +[devenv.target.win.constraints] +vc = ">=14" +``` + +Default `constraints`. They are inherited by default by downstream projects. + +`constraints` contain version specs similar to `[dependencies]`, but contrary to `dependencies` the specs are not part of the environment by default. + +They will be added to the versions specifiers of the section *if* a downstream project explicitly declares that dependency. + + +```toml +[devenv.env-vars] +# Lists are prepended to existing variable of same name, with the appropriate joiner for the platform (':' on Linux, ';' on Windows). +# $project_dir is replaced by the project directory. +PYTHONPATH = ['$project_dir/src'] + +# Alternatives where it is possible to control if prepend or append. +# PYTHONPATH.append = ['{{ project_dir }}/src'] +# PYTHONPATH.prepend = ['{{ project_dir }}/src'] + +# Strings are set directly. +JOBS = "6" + +# Overwrite by platform uses the same syntax as usual. +[devenv.target.unix.env-vars] +CC = 'CC $CC' ``` -Projects are included using paths to their directories, relative to the current file, as opposed to referencing a `.devenv` file (like `conda-devenv`). +Environment variables (note this is different from *environments*). By default they are inherited. -The reason is that `pixi-devenv` only supports a single `pixi.devenv.toml` file per project. Multiple environment and build variantes are contained all in the `pixi.devenv.toml` file, so there is no need for multiple `devenv` files. +This takes the place of the `[activation]` section of the default pixi configuration. -To enable future extensions, this syntax is also valid: ```toml -includes = [ +[devenv.feature.python310] +dependencies = { python = "3.10.*" } +env-vars = { CONDA_PY = "310" } + +[devenv.feature.python312] +dependencies = { python = "3.12.*" } +env-vars = { CONDA_PY = "312" } + +[devenv.feature.test] +dependencies = { pytest = "*" } + +[devenv.feature.compile] +dependencies = { cmake = "*" } +``` + +Feature configuration, identical to pixi's `[feature]` section. Features **are not** inherited automatically. The reason for that is that features that are not used by environments generate a warning, which would cause false warnings in downstream projects only because they decide to not use a feature available on upstream projects. + + +```toml +[devenv.environment] +py310 = ["python310"] +py310-test = ["python310", "test", "compile"] +py312 = ["python312"] +py312-test = ["python312", "test", "compile"] +``` + +Environment configuration, identical to pixi's `[environment]` section. Same as features, environments **are not inherited** by default. + + +### `calc/pixi.devenv.toml` + + +```toml +[devenv] +name = "calc" +# platforms = ["linux-64"] # can overwrite platforms defined upstream. +# channels = ["conda-forge"] # can overwrite platforms defined upstream. + + +# Mandatory: List of upstream projects. This should be a list pointing to the directory, relative to this directory, of the upstream's project `pixi.devenv.toml` file. +upstream = [ "../core", - { path="../calc" }, ] + +[devenv.dependencies] + + + +[devenv.inherit] # Optional +# Both settings can be a list instead of a bool, meaning to inherit dependencies only from the projects explicitly listed. +# dependencies = ["core"] +# Default to true, meaning default dependencies from all upstream projects are inherited. Using false means no dependencies are inherited. +dependencies = true +pypi-dependencies = true +env-vars = true + +# Controls which features will be inherited. By default this table is empty, meaning no features are inherited. +[devenv.inherit.features] # Optional +py310 = true # inherits all features defined upstream named 'py310'. +# py310-test = ['core'] # instead of inheriting all features of the same name, you can inherit the feature only from specific upstream projects. ``` +Note: `environments` **are never inherited**. + + + +## Differences to `conda-devenv` + +[conda-devenv](https://github.com/ESSS/pixi-devenv) is a tool developed by ESSS with the same purpose as `pixi-devenv`: working with multiple projects in development mode. + +There is one important difference on how the tools work: + +`conda-devenv`, on one hand, is a frontend tool. Developers work with it directly on their day to day work, even if they are not changing dependencies or adding/removing projects -- developers call `conda devenv` to create their environments. One consequence of this is that developers need to have `conda-devenv` installed in their root `conda` installation, which requires everyone to be using the exact same version `conda` version, because unfortunately bugs in conda happen (as in any software). The lack of native locking in `conda` requires using `conda-lock`, which by itself must also be of a compatible version with `conda` and `conda-devenv`, further complicating bootstrapping requirements. + + +`pixi-devenv`, on the other hand, is a code generation tool. You don't need to use it on your day to day work, because you deal with a plain `pixi.toml` file, using `pixi` directly. You only need `pixi-devenv` when you make changes to the dependencies of the project, or add/remove upstream projects -- in that case, you invoke `pixi-devenv` to update your `pixi.toml` file. This allows `pixi-devenv` to be implemented like any other tool, resolving the bootstrapping problem. + diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..ad70773 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +files = src,tests +strict = true + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3ea6cf7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "pixi-devenv" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "ESSS", email = "dev@esss.com" } +] +requires-python = ">=3.12" +dependencies = [ + "pyserde[toml]>=0.25.0", + "tomlkit>=0.13.3", + "typer>=0.17.3", +] + +[project.scripts] +pixi-devenv = "pixi_devenv.cli:app" + +[build-system] +requires = ["uv_build>=0.8.13,<0.9.0"] +build-backend = "uv_build" + +[dependency-groups] +dev = [ + "mypy>=1.17.1", + "pre-commit>=4.3.0", + "pytest>=8.4.2", + "pytest-regressions>=2.8.3", + "ruff>=0.12.12", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..0bc5130 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = tests +addopts = -ra diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..2806434 --- /dev/null +++ b/ruff.toml @@ -0,0 +1 @@ +line-length = 110 diff --git a/src/pixi_devenv/__init__.py b/src/pixi_devenv/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pixi_devenv/__main__.py b/src/pixi_devenv/__main__.py new file mode 100644 index 0000000..e4caf82 --- /dev/null +++ b/src/pixi_devenv/__main__.py @@ -0,0 +1,5 @@ +from pixi_devenv.cli import app + + +if __name__ == "__main__": + app() diff --git a/src/pixi_devenv/cli.py b/src/pixi_devenv/cli.py new file mode 100644 index 0000000..c341012 --- /dev/null +++ b/src/pixi_devenv/cli.py @@ -0,0 +1,31 @@ +from pathlib import Path + +from typer import Typer +import rich + +from pixi_devenv.update import update_pixi_config + +app = Typer() + + +@app.command() +def update(path: Path | None = None) -> None: + """Update pixi configuration in the given directory (defaults to cwd)""" + updated = update_pixi_config(path or Path.cwd()) + if updated: + rich.print("[green]pixi configuration updated[/green]") + else: + rich.print("pixi configuration already up to date") + + +@app.command() +def init() -> None: + """Initialize pixi-devenv configuration in this directory (TODO)""" + print("TODO") + + +@app.command() +def import_from_conda(file: str = "environment.devenv.yml", feature: str = "") -> None: + """Import conda-devenv configuration into a pixi.devenv.toml file""" + # 'feature' is used to import the environment.devenv.yml contents as a feature. + print(f"TODO {file} {feature}") diff --git a/src/pixi_devenv/consolidate.py b/src/pixi_devenv/consolidate.py new file mode 100644 index 0000000..a2fa031 --- /dev/null +++ b/src/pixi_devenv/consolidate.py @@ -0,0 +1,366 @@ +from __future__ import annotations + +import os +import string +from collections import defaultdict +from collections.abc import Iterator +from dataclasses import dataclass, field +from enum import Enum, auto +from pathlib import PurePath, Path +from typing import assert_never, Sequence + +from pixi_devenv.project import Project, Spec, ProjectName, EnvVarValue, DevEnvError, Aspect, Feature +from pixi_devenv.workspace import Workspace + + +class Shell(Enum): + Cmd = auto() + Bash = auto() + + def env_var(self, name: str) -> str: + match self: + case Shell.Cmd: + return f"%{name}%" + case Shell.Bash: + return f"${{{name}}}" + case unreachable: + assert_never(unreachable) + + def define_keyword(self) -> str: + match self: + case Shell.Cmd: + return "set" + case Shell.Bash: + return "export" + case unreachable: + assert_never(unreachable) + + def path_separator(self) -> str: + match self: + case Shell.Cmd: + return ";" + case Shell.Bash: + return ":" + case unreachable: + assert_never(unreachable) + + @classmethod + def from_target_name(cls, target_name: str) -> Shell: + if target_name.startswith("win"): + return Shell.Cmd + else: + return Shell.Bash + + +def consolidate_devenv(workspace: Workspace) -> ConsolidatedProject: + root_aspect = _consolidate_aspects( + workspace, [(p, p.get_root_aspect()) for p in workspace.iter_downstream()] + ) + + consolidated_target = _consolidate_target(workspace, list(workspace.iter_downstream())) + consolidated_feature = _consolidate_feature(workspace) + + channels: tuple[str, ...] = () + platforms: tuple[str, ...] = () + + for project in workspace.iter_downstream(): + if project.channels: + channels = project.channels + if project.platforms: + platforms = project.platforms + + return ConsolidatedProject( + name=workspace.starting_project.name, + channels=channels, + platforms=platforms, + dependencies=root_aspect.dependencies, + pypi_dependencies=root_aspect.pypi_dependencies, + env_vars=root_aspect.env_vars, + target=consolidated_target, + feature=consolidated_feature, + ) + + +@dataclass +class ConsolidatedProject: + name: str + + channels: tuple[str, ...] = () + platforms: tuple[str, ...] = () + + dependencies: dict[str, MergedSpec] = field(default_factory=dict) + pypi_dependencies: dict[str, MergedSpec] = field(default_factory=dict) + env_vars: dict[str, MergedEnvVarValue] = field(default_factory=dict) + target: dict[str, ConsolidatedAspect] = field(default_factory=dict) + feature: dict[str, ConsolidatedFeature] = field(default_factory=dict) + + +type Sources = tuple[ProjectName, ...] + + +@dataclass(frozen=True) +class MergedSpec: + sources: Sources + spec: Spec + + def add(self, spec_name: str, sources: ProjectName | tuple[ProjectName, ...], spec: Spec) -> MergedSpec: + if self.spec.version != "*" and spec.version != "*": + version = f"{self.spec.version},{spec.version}" + elif self.spec.version != "*": + version = self.spec.version + else: + version = spec.version + + if self.spec.build and spec.build and self.spec.build != spec.build: + raise DevEnvError( + f"Conflicting builds declared for {spec_name} in {self.sources} and {sources}: {self.spec.build}, {spec.build}" + ) + build = self.spec.build or spec.build + + if not isinstance(sources, tuple): + sources = (sources,) + + if self.spec.channel and spec.channel and self.spec.channel != spec.channel: + raise DevEnvError( + f"Conflicting channels declared for {spec_name} in {self.sources} and {sources}: {self.spec.channel}, {spec.channel}" + ) + channel = self.spec.channel or spec.channel + + return MergedSpec( + sources=self.sources + sources, + spec=Spec(version=version, build=build, channel=channel), + ) + + +@dataclass(frozen=True) +class ResolvedEnvVar: + value: EnvVarValue + + @classmethod + def resolve(cls, project: Project, ws: Workspace, value: EnvVarValue) -> ResolvedEnvVar: + relative = project.directory.relative_to(ws.starting_project.directory) + normalized = Path(os.path.normpath(relative)) + mapping = { + "devenv_project_dir": PurePath("${PIXI_PROJECT_ROOT}", normalized).as_posix(), + } + + def replace_devenv_vars(s: str) -> str: + return s.format(**mapping) + + match value: + case str() as single_value: + return ResolvedEnvVar(replace_devenv_vars(single_value)) + case tuple() as values: + return ResolvedEnvVar(tuple(replace_devenv_vars(x) for x in values)) + case unreachable: + assert_never(unreachable) + + +@dataclass(frozen=True) +class MergedEnvVarValue: + sources: Sources + var: ResolvedEnvVar + + def merge(self, other: MergedEnvVarValue) -> MergedEnvVarValue: + sources = self.sources + other.sources + match other.var.value: + case str(): + if not isinstance(self.var.value, str): + raise DevEnvError( + f"Incompatible env-var definition, they should have the same type: {other.var.value!r} vs {self.var.value!r}" + ) + return MergedEnvVarValue(sources=sources, var=other.var) + case tuple(): + if not isinstance(self.var.value, tuple): + raise DevEnvError( + f"Incompatible env-var definition, they should have the same type: {other.var.value!r} vs {self.var.value!r}" + ) + new_values = ResolvedEnvVar(self.var.value + other.var.value) + return MergedEnvVarValue(sources=sources, var=new_values) + case unreachable: + assert_never(unreachable) + + def get_generic_value(self) -> str | None: + match self.var.value: + case str(v): + t = string.Template(v) + # Without any identifiers: does not require platform-specific replacements, so it is generic. + if not t.get_identifiers(): + return v + else: + # Requires platform-specific replacement of the variables: + # "$FOO/lib" -> "${FOO}/lib" or "%FOO%/lib" + return None + case tuple(): + # Lists always require platform-specific versions, to join them using the appropraite + # path separator (':' or ';'). + return None + case unreachable: + assert_never(unreachable) + + +@dataclass +class ConsolidatedAspect: + dependencies: dict[str, MergedSpec] + pypi_dependencies: dict[str, MergedSpec] + env_vars: dict[str, MergedEnvVarValue] + + +@dataclass +class ConsolidatedFeature: + dependencies: dict[str, MergedSpec] + pypi_dependencies: dict[str, MergedSpec] + env_vars: dict[str, MergedEnvVarValue] + target: dict[str, ConsolidatedAspect] + + def is_empty(self) -> bool: + return not self.dependencies and not self.pypi_dependencies and not self.env_vars and not self.target + + +@dataclass(frozen=True) +class FeatureWithProject: + project: Project + feature: Feature + + @property + def target(self) -> dict[str, Aspect]: + return self.feature.target + + +def get_project_name(p: Project | FeatureWithProject) -> ProjectName: + match p: + case Project(): + return p.name + case FeatureWithProject(project=project): + return project.name + case unreachable: + assert_never(unreachable) + + +def _consolidate_aspects( + workspace: Workspace, + aspects: Sequence[tuple[Project | FeatureWithProject, Aspect]], +) -> ConsolidatedAspect: + def update_specs( + project_name: ProjectName, + dependencies_dict: dict[str, MergedSpec], + specs: Iterator[tuple[str, Spec]], + ) -> None: + for name, spec in specs: + try: + merged_spec = dependencies_dict[name] + dependencies_dict[name] = merged_spec.add(name, project_name, spec) + except KeyError: + merged_spec = MergedSpec((project_name,), spec) + if constraint := constraints.get(name): + merged_spec = merged_spec.add(name, constraint.sources, constraint.spec) + dependencies_dict[name] = merged_spec + + constraints: dict[str, MergedSpec] = {} + + starting_project = workspace.starting_project + inherit = starting_project.inherit + + for project_or_feature, aspect in aspects: + project_name = get_project_name(project_or_feature) + inherit_this_constraints = inherit.use_dependencies( + project_name, starting_project + ) or inherit.use_pypi_dependencies(project_name, starting_project) + if inherit_this_constraints: + update_specs( + project_name, constraints, ((n, Spec.normalized(s)) for (n, s) in aspect.constraints.items()) + ) + + dependencies: dict[str, MergedSpec] = {} + pypi_dependencies: dict[str, MergedSpec] = {} + + for project_or_feature, aspect in aspects: + project_name = get_project_name(project_or_feature) + if inherit.use_dependencies(project_name, starting_project): + update_specs( + project_name, + dependencies, + ((n, Spec.normalized(s)) for (n, s) in aspect.dependencies.items()), + ) + if inherit.use_pypi_dependencies(project_name, starting_project): + update_specs( + project_name, + pypi_dependencies, + ((n, Spec.normalized(s)) for (n, s) in aspect.pypi_dependencies.items()), + ) + + result_env_vars: dict[str, MergedEnvVarValue] = {} + + for project_or_feature, aspect in aspects: + project_name = get_project_name(project_or_feature) + if not inherit.use_env_vars(project_name, starting_project): + continue + + match project_or_feature: + case Project(): + project = project_or_feature + case FeatureWithProject(project=p): + project = p + case unreachable: + assert_never(unreachable) + + for name, env_var in aspect.env_vars.items(): + evaluated_env_var = ResolvedEnvVar.resolve(project, workspace, env_var) + merged = MergedEnvVarValue(sources=(project_name,), var=evaluated_env_var) + try: + evaluated = result_env_vars[name] + result_env_vars[name] = evaluated.merge(merged) + except KeyError: + result_env_vars[name] = merged + + return ConsolidatedAspect( + dependencies=dependencies, pypi_dependencies=pypi_dependencies, env_vars=result_env_vars + ) + + +def _consolidate_target( + workspace: Workspace, projects: Sequence[Project | FeatureWithProject] +) -> dict[str, ConsolidatedAspect]: + all_targets = defaultdict[str, list[Project | FeatureWithProject]](list) + for project in projects: + for target_name in project.target: + all_targets[target_name].append(project) + + consolidated_aspect = dict[str, ConsolidatedAspect]() + for target_name, projects in all_targets.items(): + aspect = _consolidate_aspects(workspace, [(p, p.target[target_name]) for p in projects]) + consolidated_aspect[target_name] = aspect + return consolidated_aspect + + +def _consolidate_feature(workspace: Workspace) -> dict[str, ConsolidatedFeature]: + all_features = defaultdict[str, list[Project]](list) + for project in workspace.iter_downstream(): + for feature_name in project.feature: + all_features[feature_name].append(project) + + starting_project = workspace.starting_project + inherit = starting_project.inherit + + result = dict[str, ConsolidatedFeature]() + for feature_name, projects in all_features.items(): + aspects_and_projects = [] + features_with_projects = [] + + for project in projects: + if inherit.use_feature(feature_name, project.name, starting_project): + aspects_and_projects.append((project, project.feature[feature_name].get_aspect())) + features_with_projects.append(FeatureWithProject(project, project.feature[feature_name])) + + aspect = _consolidate_aspects(workspace, aspects_and_projects) + target = _consolidate_target(workspace, features_with_projects) + + consolidated_feature = ConsolidatedFeature( + dependencies=aspect.dependencies, + pypi_dependencies=aspect.pypi_dependencies, + env_vars=aspect.env_vars, + target=target, + ) + if not consolidated_feature.is_empty(): + result[feature_name] = consolidated_feature + return result diff --git a/src/pixi_devenv/project.py b/src/pixi_devenv/project.py new file mode 100644 index 0000000..fac3793 --- /dev/null +++ b/src/pixi_devenv/project.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass +from pathlib import Path +from typing import Union, Any, NewType, assert_never + +import serde.toml + + +class DevEnvError(Exception): + pass + + +ProjectName = NewType("ProjectName", str) + + +@dataclass +class Root: + devenv: Project + + +@dataclass +class Upstream: + path: str + + @classmethod + def normalized(cls, upstream: str | Upstream) -> Upstream: + match upstream: + case str() as path: + return Upstream(path) + case Upstream(): + return upstream + case unreachable: + assert_never(unreachable) + + +@dataclass +class Spec: + version: str + build: str = "" + channel: str = "" + + @classmethod + def normalized(cls, spec: Spec | str) -> Spec: + match spec: + case str() as version: + return Spec(version=version) + case Spec(): + return spec + case unreachable: + assert_never(unreachable) + + def is_version_only(self) -> bool: + return not self.build and not self.channel + + +EnvVarValue = Union[str, tuple[str, ...]] + + +@serde.serde(tagging=serde.Untagged) +@dataclass +class Aspect: + dependencies: dict[str, Spec | str] = serde.field(default_factory=dict) + pypi_dependencies: dict[str, Spec | str] = serde.field(rename="pypi-dependencies", default_factory=dict) + constraints: dict[str, Spec | str] = serde.field(default_factory=dict) + env_vars: dict[str, EnvVarValue] = serde.field(rename="env-vars", default_factory=dict) + + +@serde.serde(tagging=serde.Untagged) +class Feature: + dependencies: dict[str, Spec | str] = serde.field(default_factory=dict) + pypi_dependencies: dict[str, Spec | str] = serde.field(rename="pypi-dependencies", default_factory=dict) + constraints: dict[str, Spec | str] = serde.field(default_factory=dict) + env_vars: dict[str, EnvVarValue] = serde.field(rename="env-vars", default_factory=dict) + target: dict[str, Aspect] = serde.field(default_factory=dict) + + def get_aspect(self) -> Aspect: + return Aspect( + dependencies=self.dependencies, + pypi_dependencies=self.pypi_dependencies, + constraints=self.constraints, + env_vars=self.env_vars, + ) + + +@serde.serde(tagging=serde.Untagged) +class Include: + include: tuple[ProjectName, ...] + + +@serde.serde(tagging=serde.Untagged) +class Exclude: + exclude: tuple[ProjectName, ...] + + +@serde.serde(tagging=serde.Untagged) +@dataclass +class Inheritance: + dependencies: bool | Include | Exclude = True + pypi_dependencies: bool | Include | Exclude = serde.field(rename="pypi-dependencies", default=True) + env_vars: bool | Include | Exclude = serde.field(rename="env-vars", default=True) + features: dict[str, bool | Include | Exclude] = serde.field(default_factory=dict) + + def use_dependencies(self, name: ProjectName, starting_project: Project) -> bool: + return self._evaluate_for_project(self.dependencies, name, starting_project) + + def use_pypi_dependencies(self, name: ProjectName, starting_project: Project) -> bool: + return self._evaluate_for_project(self.pypi_dependencies, name, starting_project) + + def use_env_vars(self, name: ProjectName, starting_project: Project) -> bool: + return self._evaluate_for_project(self.env_vars, name, starting_project) + + def use_feature(self, feature_name: str, project_name: ProjectName, starting_project: Project) -> bool: + if feature_name in starting_project.feature: + return True + if include_exclude := self.features.get(feature_name): + return self._evaluate_for_project(include_exclude, project_name, starting_project) + return False + + @staticmethod + def _evaluate_for_project( + include_exclude: bool | Include | Exclude, name: ProjectName, starting_project: Project + ) -> bool: + if name == starting_project.name: + return True + match include_exclude: + case Include(include): + return name in include + case Exclude(exclude): + return name not in exclude + case bool() as include: + return include + case unreachable: + assert_never(unreachable) + + +@serde.serde(tagging=serde.Untagged) +@dataclass +class Project: + # `name` exists only to raise an error if it is actually defined in the file: the name is defined + # by the directory where the toml file resides. + _name: str | None = serde.field(rename="name", default=None) + # `environment` exists only to raise an error if it is actually defined in the file: environments are not manipulated + # by pixi-devenv and should be defined directly in pixi.toml. + environments: dict[str, Any] | None = None + + channels: tuple[str, ...] = () + platforms: tuple[str, ...] = () + + upstream: tuple[str | Upstream, ...] = () + dependencies: dict[str, Spec | str] = serde.field(default_factory=dict) + pypi_dependencies: dict[str, Spec | str] = serde.field(rename="pypi-dependencies", default_factory=dict) + constraints: dict[str, Spec | str] = serde.field(default_factory=dict) + env_vars: dict[str, EnvVarValue] = serde.field(rename="env-vars", default_factory=dict) + target: dict[str, Aspect] = serde.field(default_factory=dict) + feature: dict[str, Feature] = serde.field(default_factory=dict) + inherit: Inheritance = serde.field(default_factory=Inheritance) + + filename: Path = serde.field(skip=True, init=False) + + @property + def name(self) -> ProjectName: + assert self._name is not None + return ProjectName(self._name) + + @property + def directory(self) -> Path: + return self.filename.parent + + @classmethod + def from_file(cls, path: Path) -> Project: + root = serde.toml.from_toml(Root, path.read_text(encoding="UTF-8")) + if root.devenv._name is not None: + raise DevEnvError( + f"In file {path}:\ndevenv.name should not be defined explicitly, it is derived from the directory name." + ) + if root.devenv.environments is not None: + raise DevEnvError( + f"In file {path}:\ndevenv.environments table should not be defined in pixi.devenv.toml, define directly in pixi.toml." + ) + root.devenv.filename = path.absolute() + root.devenv._name = ProjectName(path.parent.name) + return root.devenv + + def iter_upstream(self) -> Iterator[Upstream]: + yield from (Upstream.normalized(x) for x in self.upstream) + + def get_root_aspect(self) -> Aspect: + return Aspect( + dependencies=self.dependencies, + pypi_dependencies=self.pypi_dependencies, + constraints=self.constraints, + env_vars=self.env_vars, + ) diff --git a/src/pixi_devenv/py.typed b/src/pixi_devenv/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/pixi_devenv/update.py b/src/pixi_devenv/update.py new file mode 100644 index 0000000..8e5d8f5 --- /dev/null +++ b/src/pixi_devenv/update.py @@ -0,0 +1,214 @@ +import dataclasses +import string +from collections.abc import Mapping +from pathlib import Path +from typing import assert_never + +import tomlkit.api +from tomlkit.items import Table + + +from pixi_devenv.consolidate import ( + consolidate_devenv, + ConsolidatedProject, + MergedSpec, + MergedEnvVarValue, + Shell, + ConsolidatedFeature, +) +from pixi_devenv.project import DevEnvError +from pixi_devenv.workspace import Workspace + + +def update_pixi_config(root: Path) -> bool: + starting_file = root / "pixi.devenv.toml" + target_file = root / "pixi.toml" + + if not starting_file.is_file(): + raise DevEnvError(f"{starting_file.name} not found in {root}.\nConsider running pixi-devenv init.") + + if not target_file.is_file(): + raise DevEnvError(f"{target_file.name} not found in {root}.\nConsider running pixi-devenv init.") + + consolidated = consolidate_devenv(Workspace.from_starting_file(starting_file)) + contents = target_file.read_text(encoding="UTF-8") + new_contents = _update_pixi_contents(root, contents, consolidated) + if contents != new_contents: + target_file.write_text(new_contents, encoding="UTF-8") + return True + else: + return False + + +def _update_pixi_contents(root: Path, contents: str, consolidated: ConsolidatedProject) -> str: + doc = tomlkit.parse(contents) + + _update_workspace_fields(doc, consolidated) + tables = _get_project_or_feature_tables(consolidated) + for name, table in tables.items(): + doc[name] = table + + features_table = _make_table() + for feature_name, feature in consolidated.feature.items(): + tables = _get_project_or_feature_tables(feature) + features_table[feature_name] = tables + + if features_table: + doc["feature"] = features_table + + new_contents = tomlkit.dumps(doc) + return new_contents + + +_MANAGED_COMMENT = "Managed by devenv" + + +def _update_workspace_fields(doc: tomlkit.TOMLDocument, consolidated: ConsolidatedProject) -> None: + doc["workspace"]["name"] = consolidated.name # type:ignore[index] + doc["workspace"]["name"].comment(_MANAGED_COMMENT) # type:ignore[index, union-attr] + + doc["workspace"]["channels"] = consolidated.channels # type:ignore[index] + doc["workspace"]["channels"].comment(_MANAGED_COMMENT) # type:ignore[index, union-attr] + + doc["workspace"]["platforms"] = consolidated.platforms # type:ignore[index] + doc["workspace"]["platforms"].comment(_MANAGED_COMMENT) # type:ignore[index, union-attr] + + +def _get_project_or_feature_tables( + consolidated: ConsolidatedProject | ConsolidatedFeature, +) -> dict[str, Table]: + result: dict[str, Table] = {} + if table := _create_dependencies_table(consolidated.dependencies): + result["dependencies"] = table + if table := _create_dependencies_table(consolidated.pypi_dependencies): + result["pypi-dependencies"] = table + + grouped = _split_env_vars(consolidated.env_vars) + + if grouped.generic: + env_table = _make_table() + env_table.update(grouped.generic) + + activation_table = _make_table() + activation_table["env"] = env_table + result["activation"] = activation_table + + target_table = tomlkit.table() + target_table.comment(_MANAGED_COMMENT) + + platform_specific_by_target = {} + + if grouped.platform_specific: + platform_specific_by_target["unix"] = grouped.platform_specific + platform_specific_by_target["win"] = grouped.platform_specific + + if consolidated.target or grouped.platform_specific: + for target_name, aspect in consolidated.target.items(): + current_target_table = _make_table() + target_table[target_name] = current_target_table + + if table := _create_dependencies_table(aspect.dependencies): + current_target_table["dependencies"] = table + if table := _create_dependencies_table(aspect.pypi_dependencies): + current_target_table["pypi-dependencies"] = table + + env_vars = dict(aspect.env_vars) + if (existing_section := platform_specific_by_target.pop(target_name, None)) is not None: + env_vars = dict(_merge_env_vars(env_vars, existing_section)) + + if env_vars: + env_table = _make_table() + env_table["env"] = _render_env_vars(target_name, env_vars) + current_target_table["activation"] = env_table + + for target_name, env_vars in platform_specific_by_target.items(): + current_target_table = _make_table() + target_table[target_name] = current_target_table + + env_table = _make_table() + env_table["env"] = _render_env_vars(target_name, env_vars) + current_target_table["activation"] = env_table + + if target_table: + result["target"] = target_table + + return result + + +def _make_table() -> Table: + table = tomlkit.table() + table.comment(_MANAGED_COMMENT) + return table + + +@dataclasses.dataclass +class GroupedEnvironmentVariables: + generic: dict[str, str] + platform_specific: dict[str, MergedEnvVarValue] + + +def _merge_env_vars( + b: Mapping[str, MergedEnvVarValue], a: Mapping[str, MergedEnvVarValue] +) -> Mapping[str, MergedEnvVarValue]: + result = dict(b.items()) + + for name in set(a).intersection(b): + result[name] = a[name].merge(b[name]) + + for name in a.keys() - b.keys(): + result[name] = a[name] + return result + + +def _render_env_vars(target_name: str, env_vars: Mapping[str, MergedEnvVarValue]) -> Table: + def substitute(value: str) -> str: + template = string.Template(value) + replacements = template.get_identifiers() + mapping = {x: shell.env_var(x) for x in replacements} + return template.safe_substitute(mapping) + + shell = Shell.from_target_name(target_name) + rendered_vars: dict[str, str] = {} + for name, env_var in sorted(env_vars.items()): + match env_var.var.value: + case str(value): + rendered_vars[name] = substitute(value) + case tuple(values): + substituted_values = [substitute(x) for x in values] + [shell.env_var(name)] + rendered_vars[name] = shell.path_separator().join(substituted_values) + case unreachable: + assert_never(unreachable) + + result = _make_table() + result.update(rendered_vars) + return result + + +def _split_env_vars(vars: Mapping[str, MergedEnvVarValue]) -> GroupedEnvironmentVariables: + generic = {} + platform_specific = {} + for name, merged_env_var in vars.items(): + if (generic_value := merged_env_var.get_generic_value()) is not None: + generic[name] = generic_value + else: + platform_specific[name] = merged_env_var + return GroupedEnvironmentVariables(generic=generic, platform_specific=platform_specific) + + +def _create_dependencies_table(deps: Mapping[str, MergedSpec]) -> Table | None: + result = tomlkit.table() + result.comment(_MANAGED_COMMENT) + + for name, merged_spec in deps.items(): + if merged_spec.spec.is_version_only(): + result.add(name, merged_spec.spec.version) + else: + inline_table = tomlkit.inline_table() + inline_table.comment(_MANAGED_COMMENT) + # Do not output values that are empty, it does not mean the same as "*". + dict_spec = {k: v for (k, v) in dataclasses.asdict(merged_spec.spec).items() if v} + inline_table.update(dict_spec) + result.add(name, inline_table) + result[name].comment(f"From: {', '.join(merged_spec.sources)}") + + return result diff --git a/src/pixi_devenv/workspace.py b/src/pixi_devenv/workspace.py new file mode 100644 index 0000000..34cfd04 --- /dev/null +++ b/src/pixi_devenv/workspace.py @@ -0,0 +1,52 @@ +import graphlib +from collections.abc import Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Self, Mapping, Iterator + +from pixi_devenv.project import ProjectName, Project, DevEnvError + + +@dataclass +class Workspace: + starting_project: Project + + projects: Mapping[ProjectName, Project] + + # Project -> their direct Upstream projects. + graph: Mapping[ProjectName, Sequence[ProjectName]] + + _upstream_to_downstream_order: Sequence[ProjectName] + + @classmethod + def from_starting_file(cls, path: Path) -> Self: + starting_project = Project.from_file(path) + to_process = [starting_project] + projects = dict[ProjectName, Project]() + graph = dict[ProjectName, list[ProjectName]]() + while to_process: + project = to_process.pop() + if project.name in projects: + continue + projects[project.name] = project + graph[project.name] = [] + + for upstream in project.iter_upstream(): + upstream_project = Project.from_file( + project.directory.joinpath(upstream.path, "pixi.devenv.toml") + ) + to_process.append(upstream_project) + graph[project.name].append(upstream_project.name) + + sorter = graphlib.TopologicalSorter(graph) + try: + upstream_to_downstream_order = list(sorter.static_order()) + except graphlib.CycleError as e: + raise DevEnvError(f"DevEnv dependencies are in a cycle: {e.args[1]}") + return cls(starting_project, projects, graph, upstream_to_downstream_order) + + def iter_downstream(self) -> Iterator[Project]: + yield from (self.projects[p] for p in self._upstream_to_downstream_order) + + def iter_upstream(self) -> Iterator[Project]: + yield from (self.projects[p] for p in reversed(self._upstream_to_downstream_order)) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..beff062 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +from pathlib import Path + +import pytest + +from tests.devenv_tester import DevEnvTester + + +@pytest.fixture +def devenv_tester(tmp_path: Path) -> DevEnvTester: + return DevEnvTester(tmp_path / "projects") diff --git a/tests/devenv_tester.py b/tests/devenv_tester.py new file mode 100644 index 0000000..43bb187 --- /dev/null +++ b/tests/devenv_tester.py @@ -0,0 +1,27 @@ +import pprint +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class DevEnvTester: + projects_path: Path + + def write_devenv(self, parent_path: str, contents: str) -> Path: + toml = self.projects_path.joinpath(f"{parent_path}/pixi.devenv.toml") + toml.parent.mkdir(parents=True, exist_ok=True) + toml.write_text(contents) + return toml + + def write_pixi(self, parent_path: str, contents: str) -> Path: + toml = self.projects_path.joinpath(f"{parent_path}/pixi.toml") + toml.parent.mkdir(parents=True, exist_ok=True) + toml.write_text(contents) + return toml + + def pprint_for_regression(self, obj: object) -> str: + contents = pprint.pformat(obj, sort_dicts=False) + contents = contents.replace(str(self.projects_path.as_posix()), "") + contents = contents.replace("WindowsPath(", "Path(") + contents = contents.replace("PosixPath(", "Path(") + return contents diff --git a/tests/test_consolidate.py b/tests/test_consolidate.py new file mode 100644 index 0000000..5aa4832 --- /dev/null +++ b/tests/test_consolidate.py @@ -0,0 +1,454 @@ +import pytest +from pytest_regressions.file_regression import FileRegressionFixture + +from pixi_devenv.consolidate import consolidate_devenv, MergedSpec +from pixi_devenv.project import ProjectName, Spec, DevEnvError +from pixi_devenv.workspace import Workspace +from tests.devenv_tester import DevEnvTester + + +def test_merged_spec() -> None: + # Collapse "*" versions -- "*,*" is valid but distracting, as is "*,>=1.0". + m = MergedSpec((ProjectName("a"),), Spec("*")) + assert m.add("lib", ProjectName("b"), Spec("*")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), Spec("*") + ) + assert m.add("lib", ProjectName("b"), Spec(">=1.0")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), Spec(">=1.0") + ) + m = MergedSpec((ProjectName("a"),), Spec(">=1.0")) + assert m.add("lib", ProjectName("b"), Spec("*")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), Spec(">=1.0") + ) + + # Add another versioned spec. + m = MergedSpec((ProjectName("a"),), Spec(">=12.0")) + assert m.add("lib", ProjectName("b"), Spec(">=13.2")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), Spec(">=12.0,>=13.2") + ) + assert m.add("lib", ProjectName("b"), Spec(">=13.2", build="b1")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), Spec(">=12.0,>=13.2", build="b1") + ) + assert m.add("lib", ProjectName("b"), Spec(">=13.2", channel="conda-forge")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), + Spec(">=12.0,>=13.2", channel="conda-forge"), + ) + + # Merge 'build'. + m2 = MergedSpec((ProjectName("a"),), Spec(">=12.0", build="b1")) + assert m2.add("lib", ProjectName("b"), Spec(">=13.2")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), Spec(">=12.0,>=13.2", build="b1") + ) + assert m2.add("lib", ProjectName("b"), Spec(">=13.2", build="b1")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), Spec(">=12.0,>=13.2", build="b1") + ) + with pytest.raises(DevEnvError, match="Conflicting builds"): + _ = m2.add("lib", ProjectName("b"), Spec(">=13.2", build="b99")) + + # Merge 'channel'. + m2 = MergedSpec((ProjectName("a"),), Spec(">=12.0", channel="ch1")) + assert m2.add("lib", ProjectName("b"), Spec(">=13.2")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), Spec(">=12.0,>=13.2", channel="ch1") + ) + assert m2.add("lib", ProjectName("b"), Spec(">=13.2", channel="ch1")) == MergedSpec( + (ProjectName("a"), ProjectName("b")), Spec(">=12.0,>=13.2", channel="ch1") + ) + with pytest.raises(DevEnvError, match="Conflicting channels"): + _ = m2.add("lib", ProjectName("b"), Spec(">=13.2", channel="ch99")) + + +def test_dependencies(devenv_tester: DevEnvTester, file_regression: FileRegressionFixture) -> None: + devenv_tester.write_devenv( + "bootstrap", + """ + [devenv.dependencies] + boltons = "24.0" + + [devenv.pypi-dependencies] + attrs = "25.0" + """, + ) + a_toml = devenv_tester.write_devenv( + "a", + """ + devenv.upstream = ["../bootstrap"] + + [devenv.dependencies] + boltons = ">=24.2" + pyqt = "*" + """, + ) + ws = Workspace.from_starting_file(a_toml) + project = consolidate_devenv(ws) + file_regression.check(devenv_tester.pprint_for_regression(project)) + + +def test_dependencies_with_constraints( + devenv_tester: DevEnvTester, file_regression: FileRegressionFixture +) -> None: + devenv_tester.write_devenv( + "bootstrap", + """ + [devenv.constraints] + pyqt = ">=5.15" + boltons = "24.0" + foobar = "1.0" # Will not appear because no downstream projects directly depend on it. + """, + ) + devenv_tester.write_devenv( + "a", + """ + devenv.upstream = ["../bootstrap"] + + [devenv.pypi-dependencies] + boltons = ">=23.0" + attrs = "*" + """, + ) + b_toml = devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../a"] + + [devenv.dependencies] + pyqt = "*" + """, + ) + ws = Workspace.from_starting_file(b_toml) + project = consolidate_devenv(ws) + file_regression.check(devenv_tester.pprint_for_regression(project)) + + +def test_dependencies_inheritance( + devenv_tester: DevEnvTester, + file_regression: FileRegressionFixture, + request: pytest.FixtureRequest, +) -> None: + devenv_tester.write_devenv( + "bootstrap", + """ + [devenv.dependencies] + boltons = ">=24.0" + """, + ) + devenv_tester.write_devenv( + "a", + """ + devenv.upstream = ["../bootstrap"] + + [devenv.dependencies] + boltons = ">=23.0" + pytest = "*" + + [devenv.pypi-dependencies] + fast-api = "*" + """, + ) + devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../a"] + + [devenv.dependencies] + coverage = "21.0" + + [devenv.pypi-dependencies] + flask = "*" + """, + ) + + c_toml = devenv_tester.write_devenv( + "c", + """ + devenv.upstream = ["../b"] + + [devenv.dependencies] + pillow = "21.0" + + [devenv.pypi-dependencies] + pytest-mock = "*" + + [devenv.inherit] + dependencies = false + pypi-dependencies = false + """, + ) + ws = Workspace.from_starting_file(c_toml) + project = consolidate_devenv(ws) + file_regression.check( + devenv_tester.pprint_for_regression(project), + basename=f"{request.node.name}_no_inheritance", + ) + + c_toml = devenv_tester.write_devenv( + "c", + """ + devenv.upstream = ["../b"] + + [devenv.dependencies] + pillow = "21.0" + + [devenv.pypi-dependencies] + pytest-mock = "*" + + [devenv.inherit] + dependencies = true + pypi-dependencies.exclude = ["a"] + """, + ) + ws = Workspace.from_starting_file(c_toml) + project = consolidate_devenv(ws) + file_regression.check( + devenv_tester.pprint_for_regression(project), + basename=f"{request.node.name}_bootstrap_inheritance", + ) + + +def test_env_vars( + devenv_tester: DevEnvTester, file_regression: FileRegressionFixture, request: pytest.FixtureRequest +) -> None: + devenv_tester.write_devenv( + "bootstrap", + """ + [devenv.env-vars] + CONDA_PY = "310" + DOCS = "{devenv_project_dir}/bootstrap-docs" + PYTHONPATH = ["{devenv_project_dir}/src"] + """, + ) + devenv_tester.write_devenv( + "a", + """ + devenv.upstream = ["../bootstrap"] + [devenv.env-vars] + PYTHONPATH = ["{devenv_project_dir}/src", "{devenv_project_dir}/artifacts-$CONDA_PY"] + """, + ) + b_toml = devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../a"] + + [devenv.env-vars] + PYTHONPATH = ["{devenv_project_dir}/src"] + DOCS = "{devenv_project_dir}/b-docs" + README = "{devenv_project_dir}/README.md" + """, + ) + ws = Workspace.from_starting_file(b_toml) + project = consolidate_devenv(ws) + file_regression.check( + devenv_tester.pprint_for_regression(project), + basename=f"{request.node.name}", + ) + + c_toml = devenv_tester.write_devenv( + "c", + """ + devenv.upstream = ["../b"] + + [devenv.env-vars] + PYTHONPATH = ["{devenv_project_dir}/src"] + + [devenv.inherit] + env-vars.exclude = ["a", "b"] + """, + ) + ws = Workspace.from_starting_file(c_toml) + project = consolidate_devenv(ws) + file_regression.check( + devenv_tester.pprint_for_regression(project), + basename=f"{request.node.name}_no_inheritance", + ) + + +def test_targets( + devenv_tester: DevEnvTester, file_regression: FileRegressionFixture, request: pytest.FixtureRequest +) -> None: + devenv_tester.write_devenv( + "bootstrap", + """ + [devenv.target.win] + dependencies = { pywin32 = "*" } + env-vars = { PLATFORM = "windows", MYPYPATH = ["{devenv_project_dir}/src"] } + + [devenv.target.linux-64] + dependencies = { sysftcl = "*" } + env-vars = { PLATFORM = "linux64" } + """, + ) + devenv_tester.write_devenv( + "a", + """ + devenv.upstream = ["../bootstrap"] + + [devenv.target.unix] + pypi-dependencies = { file-lock = "*" } + env-vars = { LOCK_MODE = "fs" } + """, + ) + b_toml = devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../a"] + + [devenv.target.win] + env-vars = { MYPYPATH = ["{devenv_project_dir}/src"] } + + [devenv.target.unix] + pypi-dependencies = { pthread = "*" } + env-vars = { PARALLEL_MODE = "threads" } + """, + ) + ws = Workspace.from_starting_file(b_toml) + project = consolidate_devenv(ws) + file_regression.check(devenv_tester.pprint_for_regression(project), basename=f"{request.node.name}") + + b_toml = devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../a"] + + [devenv.target.win] + env-vars = { MYPYPATH = ["{devenv_project_dir}/src"] } + + [devenv.target.unix] + pypi-dependencies = { pthread = "*" } + env-vars = { PARALLEL_MODE = "threads" } + + [devenv.inherit] + dependencies = false + env-vars.include = ["bootstrap"] + """, + ) + ws = Workspace.from_starting_file(b_toml) + project = consolidate_devenv(ws) + file_regression.check( + devenv_tester.pprint_for_regression(project), basename=f"{request.node.name}_inheritance" + ) + + +def test_features( + devenv_tester: DevEnvTester, file_regression: FileRegressionFixture, request: pytest.FixtureRequest +) -> None: + devenv_tester.write_devenv( + "bootstrap", + """ + [devenv.feature.py310] + dependencies = { python = "3.10.*" } + env-vars = { CONDA_PY = "310" } + + [devenv.feature.py310.target.windows] + dependencies = { pywin32 = "*" } + env-vars = { PLATFORM = "windows-py310" } + + [devenv.feature.py310.target.linux] + env-vars = { PLATFORM = "linux-py310" } + + [devenv.feature.py312] + dependencies = { python = "3.12.*" } + env-vars = { CONDA_PY = "312" } + + [devenv.feature.py312.target.windows] + dependencies = { pywin32 = "35.0" } + env-vars = { PLATFORM = "windows-py312" } + + [devenv.feature.py312.target.linux] + env-vars = { PLATFORM = "linux-py312" } + + [devenv.feature.test] + dependencies = { pytest = "*" } + """, + ) + devenv_tester.write_devenv( + "a", + """ + devenv.upstream = ["../bootstrap"] + + [devenv.feature.py310.target.windows] + dependencies = { pillow = "*" } + env-vars = { MYPYPATH = ["{devenv_project_dir}/src"] } + """, + ) + b_toml = devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../a"] + + [devenv.inherit.features] + py310.include = ["bootstrap"] + py312 = true + """, + ) + ws = Workspace.from_starting_file(b_toml) + project = consolidate_devenv(ws) + file_regression.check(devenv_tester.pprint_for_regression(project), basename=f"{request.node.name}") + + # Inherit py310 in b because we defined the feature too. + b_toml = devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../a"] + + [devenv.feature.py310.target.windows] + env-vars = { MYPYPATH = ["{devenv_project_dir}/src"] } + """, + ) + ws = Workspace.from_starting_file(b_toml) + project = consolidate_devenv(ws) + file_regression.check( + devenv_tester.pprint_for_regression(project), basename=f"{request.node.name}_inheritance" + ) + + # No feature inherited unless explicitly inherited or defining the feature. + b_toml = devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../a"] + """, + ) + ws = Workspace.from_starting_file(b_toml) + project = consolidate_devenv(ws) + file_regression.check( + devenv_tester.pprint_for_regression(project), basename=f"{request.node.name}_no_inheritance" + ) + + +def test_channels_and_platforms( + devenv_tester: DevEnvTester, file_regression: FileRegressionFixture, request: pytest.FixtureRequest +) -> None: + devenv_tester.write_devenv( + "bootstrap", + """ + [devenv] + channels = ["conda-forge"] + platforms = ["win-64", "linux-64"] + """, + ) + a_toml = devenv_tester.write_devenv( + "a", + """ + devenv.upstream = ["../bootstrap"] + """, + ) + ws = Workspace.from_starting_file(a_toml) + project = consolidate_devenv(ws) + file_regression.check( + devenv_tester.pprint_for_regression(project), basename=f"{request.node.name}_from_bootstrap" + ) + + a_toml = devenv_tester.write_devenv( + "a", + """ + [devenv] + channels = ["company.com/channel1", "company.com/channel2"] + platforms = ["linux-64"] + + upstream = ["../bootstrap"] + """, + ) + ws = Workspace.from_starting_file(a_toml) + project = consolidate_devenv(ws) + file_regression.check( + devenv_tester.pprint_for_regression(project), basename=f"{request.node.name}_overwrite" + ) diff --git a/tests/test_consolidate/test_channels_and_platforms_from_bootstrap.txt b/tests/test_consolidate/test_channels_and_platforms_from_bootstrap.txt new file mode 100644 index 0000000..e905e37 --- /dev/null +++ b/tests/test_consolidate/test_channels_and_platforms_from_bootstrap.txt @@ -0,0 +1,8 @@ +ConsolidatedProject(name='a', + channels=('conda-forge',), + platforms=('win-64', 'linux-64'), + dependencies={}, + pypi_dependencies={}, + env_vars={}, + target={}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_channels_and_platforms_overwrite.txt b/tests/test_consolidate/test_channels_and_platforms_overwrite.txt new file mode 100644 index 0000000..0bfe6c0 --- /dev/null +++ b/tests/test_consolidate/test_channels_and_platforms_overwrite.txt @@ -0,0 +1,8 @@ +ConsolidatedProject(name='a', + channels=('company.com/channel1', 'company.com/channel2'), + platforms=('linux-64',), + dependencies={}, + pypi_dependencies={}, + env_vars={}, + target={}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_dependencies.txt b/tests/test_consolidate/test_dependencies.txt new file mode 100644 index 0000000..d9563ea --- /dev/null +++ b/tests/test_consolidate/test_dependencies.txt @@ -0,0 +1,19 @@ +ConsolidatedProject(name='a', + channels=(), + platforms=(), + dependencies={'boltons': MergedSpec(sources=('bootstrap', + 'a'), + spec=Spec(version='24.0,>=24.2', + build='', + channel='')), + 'pyqt': MergedSpec(sources=('a',), + spec=Spec(version='*', + build='', + channel=''))}, + pypi_dependencies={'attrs': MergedSpec(sources=('bootstrap',), + spec=Spec(version='25.0', + build='', + channel=''))}, + env_vars={}, + target={}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_dependencies_inheritance_bootstrap_inheritance.txt b/tests/test_consolidate/test_dependencies_inheritance_bootstrap_inheritance.txt new file mode 100644 index 0000000..9a20bb7 --- /dev/null +++ b/tests/test_consolidate/test_dependencies_inheritance_bootstrap_inheritance.txt @@ -0,0 +1,31 @@ +ConsolidatedProject(name='c', + channels=(), + platforms=(), + dependencies={'boltons': MergedSpec(sources=('bootstrap', + 'a'), + spec=Spec(version='>=24.0,>=23.0', + build='', + channel='')), + 'pytest': MergedSpec(sources=('a',), + spec=Spec(version='*', + build='', + channel='')), + 'coverage': MergedSpec(sources=('b',), + spec=Spec(version='21.0', + build='', + channel='')), + 'pillow': MergedSpec(sources=('c',), + spec=Spec(version='21.0', + build='', + channel=''))}, + pypi_dependencies={'flask': MergedSpec(sources=('b',), + spec=Spec(version='*', + build='', + channel='')), + 'pytest-mock': MergedSpec(sources=('c',), + spec=Spec(version='*', + build='', + channel=''))}, + env_vars={}, + target={}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_dependencies_inheritance_no_inheritance.txt b/tests/test_consolidate/test_dependencies_inheritance_no_inheritance.txt new file mode 100644 index 0000000..fd109bd --- /dev/null +++ b/tests/test_consolidate/test_dependencies_inheritance_no_inheritance.txt @@ -0,0 +1,14 @@ +ConsolidatedProject(name='c', + channels=(), + platforms=(), + dependencies={'pillow': MergedSpec(sources=('c',), + spec=Spec(version='21.0', + build='', + channel=''))}, + pypi_dependencies={'pytest-mock': MergedSpec(sources=('c',), + spec=Spec(version='*', + build='', + channel=''))}, + env_vars={}, + target={}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_dependencies_with_constraints.txt b/tests/test_consolidate/test_dependencies_with_constraints.txt new file mode 100644 index 0000000..9a2255a --- /dev/null +++ b/tests/test_consolidate/test_dependencies_with_constraints.txt @@ -0,0 +1,19 @@ +ConsolidatedProject(name='b', + channels=(), + platforms=(), + dependencies={'pyqt': MergedSpec(sources=('b', 'bootstrap'), + spec=Spec(version='>=5.15', + build='', + channel=''))}, + pypi_dependencies={'boltons': MergedSpec(sources=('a', + 'bootstrap'), + spec=Spec(version='>=23.0,24.0', + build='', + channel='')), + 'attrs': MergedSpec(sources=('a',), + spec=Spec(version='*', + build='', + channel=''))}, + env_vars={}, + target={}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_env_vars.txt b/tests/test_consolidate/test_env_vars.txt new file mode 100644 index 0000000..d6c6401 --- /dev/null +++ b/tests/test_consolidate/test_env_vars.txt @@ -0,0 +1,21 @@ +ConsolidatedProject(name='b', + channels=(), + platforms=(), + dependencies={}, + pypi_dependencies={}, + env_vars={'CONDA_PY': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='310')), + 'DOCS': MergedEnvVarValue(sources=('bootstrap', + 'b'), + var=ResolvedEnvVar(value='${PIXI_PROJECT_ROOT}/b-docs')), + 'PYTHONPATH': MergedEnvVarValue(sources=('bootstrap', + 'a', + 'b'), + var=ResolvedEnvVar(value=('${PIXI_PROJECT_ROOT}/../bootstrap/src', + '${PIXI_PROJECT_ROOT}/../a/src', + '${PIXI_PROJECT_ROOT}/../a/artifacts-$CONDA_PY', + '${PIXI_PROJECT_ROOT}/src'))), + 'README': MergedEnvVarValue(sources=('b',), + var=ResolvedEnvVar(value='${PIXI_PROJECT_ROOT}/README.md'))}, + target={}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_env_vars_no_inheritance.txt b/tests/test_consolidate/test_env_vars_no_inheritance.txt new file mode 100644 index 0000000..122d2f5 --- /dev/null +++ b/tests/test_consolidate/test_env_vars_no_inheritance.txt @@ -0,0 +1,15 @@ +ConsolidatedProject(name='c', + channels=(), + platforms=(), + dependencies={}, + pypi_dependencies={}, + env_vars={'CONDA_PY': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='310')), + 'DOCS': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='${PIXI_PROJECT_ROOT}/../bootstrap/bootstrap-docs')), + 'PYTHONPATH': MergedEnvVarValue(sources=('bootstrap', + 'c'), + var=ResolvedEnvVar(value=('${PIXI_PROJECT_ROOT}/../bootstrap/src', + '${PIXI_PROJECT_ROOT}/src')))}, + target={}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_features.txt b/tests/test_consolidate/test_features.txt new file mode 100644 index 0000000..9e26d0b --- /dev/null +++ b/tests/test_consolidate/test_features.txt @@ -0,0 +1,43 @@ +ConsolidatedProject(name='b', + channels=(), + platforms=(), + dependencies={}, + pypi_dependencies={}, + env_vars={}, + target={}, + feature={'py310': ConsolidatedFeature(dependencies={'python': MergedSpec(sources=('bootstrap',), + spec=Spec(version='3.10.*', + build='', + channel=''))}, + pypi_dependencies={}, + env_vars={'CONDA_PY': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='310'))}, + target={'windows': ConsolidatedAspect(dependencies={'pywin32': MergedSpec(sources=('bootstrap',), + spec=Spec(version='*', + build='', + channel=''))}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='windows-py310'))}), + 'linux': ConsolidatedAspect(dependencies={}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='linux-py310'))})}), + 'py312': ConsolidatedFeature(dependencies={'python': MergedSpec(sources=('bootstrap',), + spec=Spec(version='3.12.*', + build='', + channel=''))}, + pypi_dependencies={}, + env_vars={'CONDA_PY': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='312'))}, + target={'windows': ConsolidatedAspect(dependencies={'pywin32': MergedSpec(sources=('bootstrap',), + spec=Spec(version='35.0', + build='', + channel=''))}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='windows-py312'))}), + 'linux': ConsolidatedAspect(dependencies={}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='linux-py312'))})})}) \ No newline at end of file diff --git a/tests/test_consolidate/test_features_inheritance.txt b/tests/test_consolidate/test_features_inheritance.txt new file mode 100644 index 0000000..25b9a0e --- /dev/null +++ b/tests/test_consolidate/test_features_inheritance.txt @@ -0,0 +1,33 @@ +ConsolidatedProject(name='b', + channels=(), + platforms=(), + dependencies={}, + pypi_dependencies={}, + env_vars={}, + target={}, + feature={'py310': ConsolidatedFeature(dependencies={'python': MergedSpec(sources=('bootstrap',), + spec=Spec(version='3.10.*', + build='', + channel=''))}, + pypi_dependencies={}, + env_vars={'CONDA_PY': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='310'))}, + target={'windows': ConsolidatedAspect(dependencies={'pywin32': MergedSpec(sources=('bootstrap',), + spec=Spec(version='*', + build='', + channel='')), + 'pillow': MergedSpec(sources=('a',), + spec=Spec(version='*', + build='', + channel=''))}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='windows-py310')), + 'MYPYPATH': MergedEnvVarValue(sources=('a', + 'b'), + var=ResolvedEnvVar(value=('${PIXI_PROJECT_ROOT}/../a/src', + '${PIXI_PROJECT_ROOT}/src')))}), + 'linux': ConsolidatedAspect(dependencies={}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='linux-py310'))})})}) \ No newline at end of file diff --git a/tests/test_consolidate/test_features_no_inheritance.txt b/tests/test_consolidate/test_features_no_inheritance.txt new file mode 100644 index 0000000..70fe594 --- /dev/null +++ b/tests/test_consolidate/test_features_no_inheritance.txt @@ -0,0 +1,8 @@ +ConsolidatedProject(name='b', + channels=(), + platforms=(), + dependencies={}, + pypi_dependencies={}, + env_vars={}, + target={}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_targets.txt b/tests/test_consolidate/test_targets.txt new file mode 100644 index 0000000..48cd25e --- /dev/null +++ b/tests/test_consolidate/test_targets.txt @@ -0,0 +1,38 @@ +ConsolidatedProject(name='b', + channels=(), + platforms=(), + dependencies={}, + pypi_dependencies={}, + env_vars={}, + target={'win': ConsolidatedAspect(dependencies={'pywin32': MergedSpec(sources=('bootstrap',), + spec=Spec(version='*', + build='', + channel=''))}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='windows')), + 'MYPYPATH': MergedEnvVarValue(sources=('bootstrap', + 'b'), + var=ResolvedEnvVar(value=('${PIXI_PROJECT_ROOT}/../bootstrap/src', + '${PIXI_PROJECT_ROOT}/src')))}), + 'linux-64': ConsolidatedAspect(dependencies={'sysftcl': MergedSpec(sources=('bootstrap',), + spec=Spec(version='*', + build='', + channel=''))}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='linux64'))}), + 'unix': ConsolidatedAspect(dependencies={}, + pypi_dependencies={'file-lock': MergedSpec(sources=('a',), + spec=Spec(version='*', + build='', + channel='')), + 'pthread': MergedSpec(sources=('b',), + spec=Spec(version='*', + build='', + channel=''))}, + env_vars={'LOCK_MODE': MergedEnvVarValue(sources=('a',), + var=ResolvedEnvVar(value='fs')), + 'PARALLEL_MODE': MergedEnvVarValue(sources=('b',), + var=ResolvedEnvVar(value='threads'))})}, + feature={}) \ No newline at end of file diff --git a/tests/test_consolidate/test_targets_inheritance.txt b/tests/test_consolidate/test_targets_inheritance.txt new file mode 100644 index 0000000..8196970 --- /dev/null +++ b/tests/test_consolidate/test_targets_inheritance.txt @@ -0,0 +1,30 @@ +ConsolidatedProject(name='b', + channels=(), + platforms=(), + dependencies={}, + pypi_dependencies={}, + env_vars={}, + target={'win': ConsolidatedAspect(dependencies={}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='windows')), + 'MYPYPATH': MergedEnvVarValue(sources=('bootstrap', + 'b'), + var=ResolvedEnvVar(value=('${PIXI_PROJECT_ROOT}/../bootstrap/src', + '${PIXI_PROJECT_ROOT}/src')))}), + 'linux-64': ConsolidatedAspect(dependencies={}, + pypi_dependencies={}, + env_vars={'PLATFORM': MergedEnvVarValue(sources=('bootstrap',), + var=ResolvedEnvVar(value='linux64'))}), + 'unix': ConsolidatedAspect(dependencies={}, + pypi_dependencies={'file-lock': MergedSpec(sources=('a',), + spec=Spec(version='*', + build='', + channel='')), + 'pthread': MergedSpec(sources=('b',), + spec=Spec(version='*', + build='', + channel=''))}, + env_vars={'PARALLEL_MODE': MergedEnvVarValue(sources=('b',), + var=ResolvedEnvVar(value='threads'))})}, + feature={}) \ No newline at end of file diff --git a/tests/test_project.py b/tests/test_project.py new file mode 100644 index 0000000..ee0be84 --- /dev/null +++ b/tests/test_project.py @@ -0,0 +1,81 @@ +import pytest +from pytest_regressions.file_regression import FileRegressionFixture + +from pixi_devenv.project import Project, DevEnvError +from tests.devenv_tester import DevEnvTester + + +def test_parse_complete_case(devenv_tester: DevEnvTester, file_regression: FileRegressionFixture) -> None: + contents = """ + [devenv] + upstream = [ + "../core", + { path = "../calc" } + ] + + [devenv.inherit] + dependencies = false + pypi-dependencies.exclude = ["core"] + env-vars.include = ["core"] + + [devenv.inherit.features] + py310 = true + py310-test.exclude = ["core"] + + [devenv.dependencies] + boltons = "*" + pytest = { version="*", build="a" } + + [devenv.pypy-dependencies] + pytest-mock = "*" + + [devenv.constraints] + qt = ">=5.15" + + [devenv.target.win.dependencies] + pywin32 = ">=3.20" + + [devenv.target.win.constraints] + vc = ">=14" + + [devenv.env-vars] + PYTHONPATH = ['{devenv_project_dir}/src'] + JOBS = "6" + + [devenv.feature.python310] + dependencies = { python = "3.10.*" } + constraints = { mypy = ">=1.15" } + env-vars = { CONDA_PY = "310" } + + [devenv.feature.python312] + dependencies = { python = "3.12.*" } + constraints = { mypy = ">=1.16" } + env-vars = { CONDA_PY = "312" } + + [devenv.feature.compile.target.win.dependencies] + dependency-walker = "*" + + [devenv.feature.compile.target.win.constraints] + cmake = ">=3.50" + + [devenv.feature.compile.target.unix.dependencies] + rhash = { version = ">=1.4.3", channel="https://company.com/get/conda-forge" } + """ + + toml = devenv_tester.write_devenv("gui", contents) + + project = Project.from_file(toml) + assert project.filename == toml + assert project.directory == toml.parent + + file_regression.check(devenv_tester.pprint_for_regression(project)) + + +def test_environment_error(devenv_tester: DevEnvTester) -> None: + contents = """ + [devenv.environments] + default = ["python310"] + """ + toml = devenv_tester.write_devenv("gui", contents) + with pytest.raises(DevEnvError): + Project.from_file(toml) diff --git a/tests/test_project/test_parse_complete_case.txt b/tests/test_project/test_parse_complete_case.txt new file mode 100644 index 0000000..273429a --- /dev/null +++ b/tests/test_project/test_parse_complete_case.txt @@ -0,0 +1,44 @@ +Project(_name='gui', + environments=None, + channels=(), + platforms=(), + upstream=('../core', Upstream(path='../calc')), + dependencies={'boltons': '*', + 'pytest': Spec(version='*', build='a', channel='')}, + pypi_dependencies={}, + constraints={'qt': '>=5.15'}, + env_vars={'PYTHONPATH': ('{devenv_project_dir}/src',), 'JOBS': '6'}, + target={'win': Aspect(dependencies={'pywin32': '>=3.20'}, + pypi_dependencies={}, + constraints={'vc': '>=14'}, + env_vars={})}, + feature={'python310': Feature(dependencies={'python': '3.10.*'}, + pypi_dependencies={}, + constraints={'mypy': '>=1.15'}, + env_vars={'CONDA_PY': '310'}, + target={}), + 'python312': Feature(dependencies={'python': '3.12.*'}, + pypi_dependencies={}, + constraints={'mypy': '>=1.16'}, + env_vars={'CONDA_PY': '312'}, + target={}), + 'compile': Feature(dependencies={}, + pypi_dependencies={}, + constraints={}, + env_vars={}, + target={'win': Aspect(dependencies={'dependency-walker': '*'}, + pypi_dependencies={}, + constraints={'cmake': '>=3.50'}, + env_vars={}), + 'unix': Aspect(dependencies={'rhash': Spec(version='>=1.4.3', + build='', + channel='https://company.com/get/conda-forge')}, + pypi_dependencies={}, + constraints={}, + env_vars={})})}, + inherit=Inheritance(dependencies=False, + pypi_dependencies=Exclude(exclude=('core',)), + env_vars=Include(include=('core',)), + features={'py310': True, + 'py310-test': Exclude(exclude=('core',))}), + filename=Path('/gui/pixi.devenv.toml')) \ No newline at end of file diff --git a/tests/test_update.py b/tests/test_update.py new file mode 100644 index 0000000..6bb8637 --- /dev/null +++ b/tests/test_update.py @@ -0,0 +1,93 @@ +from textwrap import dedent + +from pytest_regressions.file_regression import FileRegressionFixture + +from pixi_devenv.update import update_pixi_config +from tests.devenv_tester import DevEnvTester + + +def test_basic_update(devenv_tester: DevEnvTester, file_regression: FileRegressionFixture) -> None: + devenv_tester.write_devenv( + "bootstrap", + """ + [devenv] + channels = ["channel1", "channel2"] + platforms = ["linux-64"] + + [devenv.env-vars] + PYTHONPATH = ["{devenv_project_dir}/src"] + LD_LIBRARY_PATH = ["$CONDA_PREFIX/lib"] + MYUSER = "$USER" + MODE = "source" + + [devenv.dependencies] + boltons = "24.0" + + [devenv.pypi-dependencies] + attrs = "25.0" + + [devenv.target.unix] + dependencies = { flock = "*" } + env-vars = { FLOCK_MODE = "std", MYPYPATH = ["{devenv_project_dir}/typing"] } + + [devenv.constraints] + pyqt = ">=5.15" + + [devenv.feature.py310] + dependencies = { python = "3.10.*", typing_extensions = "*" } + env-vars = { CONDA_PY = "310" } + + [devenv.feature.py312] + dependencies = { python = "3.12.*" } + env-vars = { CONDA_PY = "312" } + """, + ) + devenv_tester.write_devenv( + "a", + """ + devenv.upstream = ["../bootstrap"] + + [devenv.env-vars] + PYTHONPATH = ["{devenv_project_dir}/src"] + MODE = "package" + + [devenv.dependencies] + boltons = ">=24.2" + pyqt = { version="*", channel="conda-forge" } + """, + ) + + devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../a"] + + [devenv.env-vars] + PYTHONPATH = ["{devenv_project_dir}/src"] + + [devenv.dependencies] + numpy = ">=2.0" + + [devenv.feature.py310.dependencies] + + [devenv.feature.py312.dependencies] + """, + ) + + pixi = devenv_tester.write_pixi( + "b", + dedent(""" + [workspace] + name = "some project" + channels = ["conda-forge"] + + [environments] + default = ["py310"] + + [dependencies] # This will be overwritten + foo = "*" + """), + ) + + update_pixi_config(pixi.parent) + file_regression.check(pixi.read_text(encoding="UTF-8")) diff --git a/tests/test_update/test_basic_update.txt b/tests/test_update/test_basic_update.txt new file mode 100644 index 0000000..8117c7a --- /dev/null +++ b/tests/test_update/test_basic_update.txt @@ -0,0 +1,47 @@ + +[workspace] +name = "b" # Managed by devenv +channels = ["channel1", "channel2"] # Managed by devenv +platforms = ["linux-64"] # Managed by devenv + +[environments] +default = ["py310"] + +[dependencies] # Managed by devenv +boltons = "24.0,>=24.2" # From: bootstrap, a +pyqt = {version = ">=5.15", channel = "conda-forge"} # From: a, bootstrap +numpy = ">=2.0" # From: b + +[pypi-dependencies] # Managed by devenv +attrs = "25.0" # From: bootstrap + +[activation.env] # Managed by devenv +MODE = "package" + +[target.unix.dependencies] # Managed by devenv +flock = "*" # From: bootstrap + +[target.unix.activation.env] # Managed by devenv +FLOCK_MODE = "std" +LD_LIBRARY_PATH = "${CONDA_PREFIX}/lib:${LD_LIBRARY_PATH}" +MYPYPATH = "${PIXI_PROJECT_ROOT}/../bootstrap/typing:${MYPYPATH}" +MYUSER = "${USER}" +PYTHONPATH = "${PIXI_PROJECT_ROOT}/../bootstrap/src:${PIXI_PROJECT_ROOT}/../a/src:${PIXI_PROJECT_ROOT}/src:${PYTHONPATH}" + +[target.win.activation.env] # Managed by devenv +LD_LIBRARY_PATH = "%CONDA_PREFIX%/lib;%LD_LIBRARY_PATH%" +MYUSER = "%USER%" +PYTHONPATH = "%PIXI_PROJECT_ROOT%/../bootstrap/src;%PIXI_PROJECT_ROOT%/../a/src;%PIXI_PROJECT_ROOT%/src;%PYTHONPATH%" + +[feature.py310.dependencies] # Managed by devenv +python = "3.10.*" # From: bootstrap +typing_extensions = "*" # From: bootstrap + +[feature.py310.activation.env] # Managed by devenv +CONDA_PY = "310" + +[feature.py312.dependencies] # Managed by devenv +python = "3.12.*" # From: bootstrap + +[feature.py312.activation.env] # Managed by devenv +CONDA_PY = "312" diff --git a/tests/test_workspace.py b/tests/test_workspace.py new file mode 100644 index 0000000..7b2d051 --- /dev/null +++ b/tests/test_workspace.py @@ -0,0 +1,177 @@ +import pytest + +from pixi_devenv.project import DevEnvError +from pixi_devenv.workspace import Workspace +from tests.devenv_tester import DevEnvTester + + +def test_standard(devenv_tester: DevEnvTester) -> None: + devenv_tester.write_devenv("bootstrap", "[devenv]") + devenv_tester.write_devenv( + "pvt", + """ + devenv.upstream = ["../bootstrap"] + """, + ) + devenv_tester.write_devenv( + "xgui", + """ + devenv.upstream = ["../bootstrap"] + """, + ) + devenv_tester.write_devenv( + "alfasim/core", + """ + devenv.upstream = ["../../pvt"] + """, + ) + devenv_tester.write_devenv( + "alfasim/calc", + """ + devenv.upstream = ["../core", "../../pvt"] + """, + ) + devenv_tester.write_devenv( + "alfasim/gui", + """ + devenv.upstream = ["../../xgui"] + """, + ) + + app_toml = devenv_tester.write_devenv( + "alfasim/app", + """ + devenv.upstream = ["../calc", "../gui"] + """, + ) + + ws = Workspace.from_starting_file(app_toml) + assert set(ws.projects) == { + "bootstrap", + "xgui", + "pvt", + "core", + "calc", + "gui", + "app", + } + assert ws.graph == { + "bootstrap": [], + "xgui": ["bootstrap"], + "pvt": ["bootstrap"], + "core": ["pvt"], + "calc": ["core", "pvt"], + "gui": ["xgui"], + "app": ["calc", "gui"], + } + assert [x.name for x in ws.iter_upstream()] == [ + "app", + "calc", + "core", + "gui", + "pvt", + "xgui", + "bootstrap", + ] + assert [x.name for x in ws.iter_downstream()] == [ + "bootstrap", + "xgui", + "pvt", + "gui", + "core", + "calc", + "app", + ] + + +def test_two_upstream_branches(devenv_tester: DevEnvTester) -> None: + devenv_tester.write_devenv("bootstrap", "[devenv]") + devenv_tester.write_devenv("bootstrap_2", "[devenv]") + devenv_tester.write_devenv( + "a", + """ + devenv.upstream = ["../bootstrap"] + """, + ) + devenv_tester.write_devenv( + "b", + """ + devenv.upstream = ["../bootstrap"] + """, + ) + devenv_tester.write_devenv( + "c", + """ + devenv.upstream = ["../bootstrap_2"] + """, + ) + + app_toml = devenv_tester.write_devenv( + "app", + """ + devenv.upstream = ["../a","../b","../c"] + """, + ) + + ws = Workspace.from_starting_file(app_toml) + assert set(ws.projects) == {"bootstrap", "bootstrap_2", "a", "b", "c", "app"} + assert ws.graph == { + "bootstrap": [], + "bootstrap_2": [], + "app": ["a", "b", "c"], + "c": ["bootstrap_2"], + "b": ["bootstrap"], + "a": ["bootstrap"], + } + assert [x.name for x in ws.iter_upstream()] == [ + "app", + "a", + "b", + "c", + "bootstrap", + "bootstrap_2", + ] + assert [x.name for x in ws.iter_downstream()] == [ + "bootstrap_2", + "bootstrap", + "c", + "b", + "a", + "app", + ] + + +def test_single_file(devenv_tester: DevEnvTester) -> None: + app_toml = devenv_tester.write_devenv( + "app", + """ + [devenv] + upstream = [] + """, + ) + + ws = Workspace.from_starting_file(app_toml) + assert set(ws.projects) == {"app"} + assert ws.graph == {"app": []} + assert [x.name for x in ws.iter_upstream()] == ["app"] + assert [x.name for x in ws.iter_downstream()] == ["app"] + + +def test_cycle(devenv_tester: DevEnvTester) -> None: + devenv_tester.write_devenv( + "a", + """ + [devenv] + upstream = ["../b"] + """, + ) + + b_toml = devenv_tester.write_devenv( + "b", + """ + [devenv] + upstream = ["../a"] + """, + ) + with pytest.raises(DevEnvError, match="DevEnv dependencies are in a cycle"): + _ = Workspace.from_starting_file(b_toml) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..66ca524 --- /dev/null +++ b/uv.lock @@ -0,0 +1,555 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "beartype" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/f9/21e5a9c731e14f08addd53c71fea2e70794e009de5b98e6a2c3d2f3015d6/beartype-0.21.0.tar.gz", hash = "sha256:f9a5078f5ce87261c2d22851d19b050b64f6a805439e8793aecf01ce660d3244", size = 1437066, upload-time = "2025-05-22T05:09:27.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/31/87045d1c66ee10a52486c9d2047bc69f00f2689f69401bb1e998afb4b205/beartype-0.21.0-py3-none-any.whl", hash = "sha256:b6a1bd56c72f31b0a496a36cc55df6e2f475db166ad07fa4acc7e74f4c7f34c0", size = 1191340, upload-time = "2025-05-22T05:09:24.606Z" }, +] + +[[package]] +name = "casefy" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/24/9c732e8e3585a1dc621c9c1349e55e87070c95d3c2d57bd8c5083ec8d731/casefy-1.1.0.tar.gz", hash = "sha256:849d6e0f80506fac70ab8e18999a4ca1eb7d8f70941682383d64aa22a7497f8f", size = 123884, upload-time = "2025-03-07T14:36:44.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/6a/1766f8c163951a3c9aeb30a4e6f5de9b2eed8389e3906c4cf30fcb475be6/casefy-1.1.0-py3-none-any.whl", hash = "sha256:a3dfcb14d85902d90702db1e9835760237f6a73ec0ae3b7e991ad767513a3cbc", size = 6539, upload-time = "2025-03-07T14:36:37.546Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "identify" +version = "2.6.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "1.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/a3/931e09fc02d7ba96da65266884da4e4a8806adcdb8a57faaacc6edf1d538/mypy-1.18.1.tar.gz", hash = "sha256:9e988c64ad3ac5987f43f5154f884747faf62141b7f842e87465b45299eea5a9", size = 3448447, upload-time = "2025-09-11T23:00:47.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/14/1c3f54d606cb88a55d1567153ef3a8bc7b74702f2ff5eb64d0994f9e49cb/mypy-1.18.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:502cde8896be8e638588b90fdcb4c5d5b8c1b004dfc63fd5604a973547367bb9", size = 12911082, upload-time = "2025-09-11T23:00:41.465Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/235606c8b6d50a8eba99773add907ce1d41c068edb523f81eb0d01603a83/mypy-1.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7509549b5e41be279afc1228242d0e397f1af2919a8f2877ad542b199dc4083e", size = 11919107, upload-time = "2025-09-11T22:58:40.903Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/4e2ce00f8d15b99d0c68a2536ad63e9eac033f723439ef80290ec32c1ff5/mypy-1.18.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5956ecaabb3a245e3f34100172abca1507be687377fe20e24d6a7557e07080e2", size = 12472551, upload-time = "2025-09-11T22:58:37.272Z" }, + { url = "https://files.pythonhosted.org/packages/32/bb/92642a9350fc339dd9dcefcf6862d171b52294af107d521dce075f32f298/mypy-1.18.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8750ceb014a96c9890421c83f0db53b0f3b8633e2864c6f9bc0a8e93951ed18d", size = 13340554, upload-time = "2025-09-11T22:59:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/38d01db91c198fb6350025d28f9719ecf3c8f2c55a0094bfbf3ef478cc9a/mypy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb89ea08ff41adf59476b235293679a6eb53a7b9400f6256272fb6029bec3ce5", size = 13530933, upload-time = "2025-09-11T22:59:20.228Z" }, + { url = "https://files.pythonhosted.org/packages/da/8d/6d991ae631f80d58edbf9d7066e3f2a96e479dca955d9a968cd6e90850a3/mypy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:2657654d82fcd2a87e02a33e0d23001789a554059bbf34702d623dafe353eabf", size = 9828426, upload-time = "2025-09-11T23:00:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ec/ef4a7260e1460a3071628a9277a7579e7da1b071bc134ebe909323f2fbc7/mypy-1.18.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d70d2b5baf9b9a20bc9c730015615ae3243ef47fb4a58ad7b31c3e0a59b5ef1f", size = 12918671, upload-time = "2025-09-11T22:58:29.814Z" }, + { url = "https://files.pythonhosted.org/packages/a1/82/0ea6c3953f16223f0b8eda40c1aeac6bd266d15f4902556ae6e91f6fca4c/mypy-1.18.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8367e33506300f07a43012fc546402f283c3f8bcff1dc338636affb710154ce", size = 11913023, upload-time = "2025-09-11T23:00:29.049Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ef/5e2057e692c2690fc27b3ed0a4dbde4388330c32e2576a23f0302bc8358d/mypy-1.18.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:913f668ec50c3337b89df22f973c1c8f0b29ee9e290a8b7fe01cc1ef7446d42e", size = 12473355, upload-time = "2025-09-11T23:00:04.544Z" }, + { url = "https://files.pythonhosted.org/packages/98/43/b7e429fc4be10e390a167b0cd1810d41cb4e4add4ae50bab96faff695a3b/mypy-1.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a0e70b87eb27b33209fa4792b051c6947976f6ab829daa83819df5f58330c71", size = 13346944, upload-time = "2025-09-11T22:58:23.024Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/899dba0bfe36bbd5b7c52e597de4cf47b5053d337b6d201a30e3798e77a6/mypy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c378d946e8a60be6b6ede48c878d145546fb42aad61df998c056ec151bf6c746", size = 13512574, upload-time = "2025-09-11T22:59:52.152Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f8/7661021a5b0e501b76440454d786b0f01bb05d5c4b125fcbda02023d0250/mypy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:2cd2c1e0f3a7465f22731987fff6fc427e3dcbb4ca5f7db5bbeaff2ff9a31f6d", size = 9837684, upload-time = "2025-09-11T22:58:44.454Z" }, + { url = "https://files.pythonhosted.org/packages/bf/87/7b173981466219eccc64c107cf8e5ab9eb39cc304b4c07df8e7881533e4f/mypy-1.18.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ba24603c58e34dd5b096dfad792d87b304fc6470cbb1c22fd64e7ebd17edcc61", size = 12900265, upload-time = "2025-09-11T22:59:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/ae/cc/b10e65bae75b18a5ac8f81b1e8e5867677e418f0dd2c83b8e2de9ba96ebd/mypy-1.18.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed36662fb92ae4cb3cacc682ec6656208f323bbc23d4b08d091eecfc0863d4b5", size = 11942890, upload-time = "2025-09-11T23:00:00.607Z" }, + { url = "https://files.pythonhosted.org/packages/39/d4/aeefa07c44d09f4c2102e525e2031bc066d12e5351f66b8a83719671004d/mypy-1.18.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:040ecc95e026f71a9ad7956fea2724466602b561e6a25c2e5584160d3833aaa8", size = 12472291, upload-time = "2025-09-11T22:59:43.425Z" }, + { url = "https://files.pythonhosted.org/packages/c6/07/711e78668ff8e365f8c19735594ea95938bff3639a4c46a905e3ed8ff2d6/mypy-1.18.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:937e3ed86cb731276706e46e03512547e43c391a13f363e08d0fee49a7c38a0d", size = 13318610, upload-time = "2025-09-11T23:00:17.604Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/df3b2d39339c31d360ce299b418c55e8194ef3205284739b64962f6074e7/mypy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f95cc4f01c0f1701ca3b0355792bccec13ecb2ec1c469e5b85a6ef398398b1d", size = 13513697, upload-time = "2025-09-11T22:58:59.534Z" }, + { url = "https://files.pythonhosted.org/packages/b1/df/462866163c99ea73bb28f0eb4d415c087e30de5d36ee0f5429d42e28689b/mypy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:e4f16c0019d48941220ac60b893615be2f63afedaba6a0801bdcd041b96991ce", size = 9985739, upload-time = "2025-09-11T22:58:51.644Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1d/4b97d3089b48ef3d904c9ca69fab044475bd03245d878f5f0b3ea1daf7ce/mypy-1.18.1-py3-none-any.whl", hash = "sha256:b76a4de66a0ac01da1be14ecc8ae88ddea33b8380284a9e3eae39d57ebcbe26e", size = 2352212, upload-time = "2025-09-11T22:59:26.576Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pixi-devenv" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "pyserde", extra = ["toml"] }, + { name = "tomlkit" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-regressions" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyserde", extras = ["toml"], specifier = ">=0.25.0" }, + { name = "tomlkit", specifier = ">=0.13.3" }, + { name = "typer", specifier = ">=0.17.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.17.1" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-regressions", specifier = ">=2.8.3" }, + { name = "ruff", specifier = ">=0.12.12" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "plum-dispatch" +version = "2.5.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/46/ab3928e864b0a88a8ae6987b3da3b7ae32fe0a610264f33272139275dab5/plum_dispatch-2.5.7.tar.gz", hash = "sha256:a7908ad5563b93f387e3817eb0412ad40cfbad04bc61d869cf7a76cd58a3895d", size = 35452, upload-time = "2025-01-17T20:07:31.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/31/21609a9be48e877bc33b089a7f495c853215def5aeb9564a31c210d9d769/plum_dispatch-2.5.7-py3-none-any.whl", hash = "sha256:06471782eea0b3798c1e79dca2af2165bafcfa5eb595540b514ddd81053b1ede", size = 42612, upload-time = "2025-01-17T20:07:26.461Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyserde" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "casefy" }, + { name = "jinja2" }, + { name = "plum-dispatch" }, + { name = "typing-extensions" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/fa708c550e6e36eb9e6f29524ea77f956e5cc89ba891bec1b89a9666d9b9/pyserde-0.25.1.tar.gz", hash = "sha256:302a534e91d548a74722bfe1577f497ed3fd87790ac9bd4503985279622e0cd0", size = 40192, upload-time = "2025-09-10T10:42:49.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/4c/5c38bbd1da35df7c37d82508c5e60beb706176261c7779a35a78d4e2862e/pyserde-0.25.1-py3-none-any.whl", hash = "sha256:7bbb4321b76662a9c2eceee88f8beb40d6fdd905aef3ae04780e06f4f3cb6f4d", size = 43965, upload-time = "2025-09-10T10:42:48.217Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli" }, + { name = "tomli-w" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-datadir" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/46/db060b291999ca048edd06d6fa9ee95945d088edc38b1172c59eeb46ec45/pytest_datadir-1.8.0.tar.gz", hash = "sha256:7a15faed76cebe87cc91941dd1920a9a38eba56a09c11e9ddf1434d28a0f78eb", size = 11848, upload-time = "2025-07-30T13:52:12.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/7a/33895863aec26ac3bb5068a73583f935680d6ab6af2a9567d409430c3ee1/pytest_datadir-1.8.0-py3-none-any.whl", hash = "sha256:5c677bc097d907ac71ca418109adc3abe34cf0bddfe6cf78aecfbabd96a15cf0", size = 6512, upload-time = "2025-07-30T13:52:11.525Z" }, +] + +[[package]] +name = "pytest-regressions" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "pytest-datadir" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/63/cdb0ee15012a538fa07de21ec0a5c8eb113db9f28378f67b538d1c0b6d04/pytest_regressions-2.8.3.tar.gz", hash = "sha256:1ad90708bee02a3d36c78ef0b6f9692a9a30d312dd828680fd6d2a7235fcd221", size = 117168, upload-time = "2025-09-05T12:51:32.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/b9/7b2fe8407744cc37a74e29bed833256a305133505ea4979564911a98338b/pytest_regressions-2.8.3-py3-none-any.whl", hash = "sha256:72500dd95bde418c850f290a3108dacb56427067f364f7112cb5b16f6d6cc29c", size = 24894, upload-time = "2025-09-05T12:51:31.1Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "ruff" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/1f4b722862840295bcaba8c9e5261572347509548faaa99b2d57ee7bfe6a/ruff-0.13.0.tar.gz", hash = "sha256:5b4b1ee7eb35afae128ab94459b13b2baaed282b1fb0f472a73c82c996c8ae60", size = 5372863, upload-time = "2025-09-10T16:25:37.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/fe/6f87b419dbe166fd30a991390221f14c5b68946f389ea07913e1719741e0/ruff-0.13.0-py3-none-linux_armv6l.whl", hash = "sha256:137f3d65d58ee828ae136a12d1dc33d992773d8f7644bc6b82714570f31b2004", size = 12187826, upload-time = "2025-09-10T16:24:39.5Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/c92296b1fc36d2499e12b74a3fdb230f77af7bdf048fad7b0a62e94ed56a/ruff-0.13.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:21ae48151b66e71fd111b7d79f9ad358814ed58c339631450c66a4be33cc28b9", size = 12933428, upload-time = "2025-09-10T16:24:43.866Z" }, + { url = "https://files.pythonhosted.org/packages/44/cf/40bc7221a949470307d9c35b4ef5810c294e6cfa3caafb57d882731a9f42/ruff-0.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64de45f4ca5441209e41742d527944635a05a6e7c05798904f39c85bafa819e3", size = 12095543, upload-time = "2025-09-10T16:24:46.638Z" }, + { url = "https://files.pythonhosted.org/packages/f1/03/8b5ff2a211efb68c63a1d03d157e924997ada87d01bebffbd13a0f3fcdeb/ruff-0.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2c653ae9b9d46e0ef62fc6fbf5b979bda20a0b1d2b22f8f7eb0cde9f4963b8", size = 12312489, upload-time = "2025-09-10T16:24:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/37/fc/2336ef6d5e9c8d8ea8305c5f91e767d795cd4fc171a6d97ef38a5302dadc/ruff-0.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cec632534332062bc9eb5884a267b689085a1afea9801bf94e3ba7498a2d207", size = 11991631, upload-time = "2025-09-10T16:24:53.439Z" }, + { url = "https://files.pythonhosted.org/packages/39/7f/f6d574d100fca83d32637d7f5541bea2f5e473c40020bbc7fc4a4d5b7294/ruff-0.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd628101d9f7d122e120ac7c17e0a0f468b19bc925501dbe03c1cb7f5415b24", size = 13720602, upload-time = "2025-09-10T16:24:56.392Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c8/a8a5b81d8729b5d1f663348d11e2a9d65a7a9bd3c399763b1a51c72be1ce/ruff-0.13.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afe37db8e1466acb173bb2a39ca92df00570e0fd7c94c72d87b51b21bb63efea", size = 14697751, upload-time = "2025-09-10T16:24:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/57/f5/183ec292272ce7ec5e882aea74937f7288e88ecb500198b832c24debc6d3/ruff-0.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f96a8d90bb258d7d3358b372905fe7333aaacf6c39e2408b9f8ba181f4b6ef2", size = 14095317, upload-time = "2025-09-10T16:25:03.025Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8d/7f9771c971724701af7926c14dab31754e7b303d127b0d3f01116faef456/ruff-0.13.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b5e3d883e4f924c5298e3f2ee0f3085819c14f68d1e5b6715597681433f153", size = 13144418, upload-time = "2025-09-10T16:25:06.272Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a6/7985ad1778e60922d4bef546688cd8a25822c58873e9ff30189cfe5dc4ab/ruff-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03447f3d18479df3d24917a92d768a89f873a7181a064858ea90a804a7538991", size = 13370843, upload-time = "2025-09-10T16:25:09.965Z" }, + { url = "https://files.pythonhosted.org/packages/64/1c/bafdd5a7a05a50cc51d9f5711da704942d8dd62df3d8c70c311e98ce9f8a/ruff-0.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fbc6b1934eb1c0033da427c805e27d164bb713f8e273a024a7e86176d7f462cf", size = 13321891, upload-time = "2025-09-10T16:25:12.969Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3e/7817f989cb9725ef7e8d2cee74186bf90555279e119de50c750c4b7a72fe/ruff-0.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8ab6a3e03665d39d4a25ee199d207a488724f022db0e1fe4002968abdb8001b", size = 12119119, upload-time = "2025-09-10T16:25:16.621Z" }, + { url = "https://files.pythonhosted.org/packages/58/07/9df080742e8d1080e60c426dce6e96a8faf9a371e2ce22eef662e3839c95/ruff-0.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2a5c62f8ccc6dd2fe259917482de7275cecc86141ee10432727c4816235bc41", size = 11961594, upload-time = "2025-09-10T16:25:19.49Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f4/ae1185349197d26a2316840cb4d6c3fba61d4ac36ed728bf0228b222d71f/ruff-0.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b7b85ca27aeeb1ab421bc787009831cffe6048faae08ad80867edab9f2760945", size = 12933377, upload-time = "2025-09-10T16:25:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/e776c10a3b349fc8209a905bfb327831d7516f6058339a613a8d2aaecacd/ruff-0.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:79ea0c44a3032af768cabfd9616e44c24303af49d633b43e3a5096e009ebe823", size = 13418555, upload-time = "2025-09-10T16:25:25.681Z" }, + { url = "https://files.pythonhosted.org/packages/46/09/dca8df3d48e8b3f4202bf20b1658898e74b6442ac835bfe2c1816d926697/ruff-0.13.0-py3-none-win32.whl", hash = "sha256:4e473e8f0e6a04e4113f2e1de12a5039579892329ecc49958424e5568ef4f768", size = 12141613, upload-time = "2025-09-10T16:25:28.664Z" }, + { url = "https://files.pythonhosted.org/packages/61/21/0647eb71ed99b888ad50e44d8ec65d7148babc0e242d531a499a0bbcda5f/ruff-0.13.0-py3-none-win_amd64.whl", hash = "sha256:48e5c25c7a3713eea9ce755995767f4dcd1b0b9599b638b12946e892123d1efb", size = 13258250, upload-time = "2025-09-10T16:25:31.773Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a3/03216a6a86c706df54422612981fb0f9041dbb452c3401501d4a22b942c9/ruff-0.13.0-py3-none-win_arm64.whl", hash = "sha256:ab80525317b1e1d38614addec8ac954f1b3e662de9d59114ecbf771d00cf613e", size = 12312357, upload-time = "2025-09-10T16:25:35.595Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "typer" +version = "0.17.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734, upload-time = "2025-09-05T18:14:40.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, +]