diff --git a/.gitignore b/.gitignore index cd25c485..b790bb39 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ coverage.xml # Sphinx documentation docs/_build/ + +.serena/cache/ diff --git a/.serena/memories/done_checklist.md b/.serena/memories/done_checklist.md new file mode 100644 index 00000000..8e0fc3e2 --- /dev/null +++ b/.serena/memories/done_checklist.md @@ -0,0 +1,16 @@ +Before considering a task done + +- Code quality + - Ruff clean: uv run ruff check . + - Types clean: uv run mypy +- Tests + - All tests green: uv run pytest + - New/changed behavior covered with tests (use project fixtures) +- Docs + - Update docs if user-facing behavior changed + - Build docs cleanly: uv run mkdocs build --clean --strict +- Packaging + - If relevant: uv run python -m build && uv run twine check dist/* +- Housekeeping + - Follow existing naming and module structure; keep functions focused and typed + - Update `CHANGELOG.md` when appropriate diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md new file mode 100644 index 00000000..cf2670d9 --- /dev/null +++ b/.serena/memories/project_overview.md @@ -0,0 +1,28 @@ +Project: setuptools-scm + +Purpose +- Extract and infer Python package versions from SCM metadata (Git/Mercurial) at build/runtime. +- Provide setuptools integrations (dynamic version, file finders) and fallbacks for archival/PKG-INFO. + +Tech Stack +- Language: Python (3.8–3.13) +- Packaging/build: setuptools (>=61), packaging; console scripts via entry points +- Tooling: uv (dependency and run), pytest, mypy (strict), ruff (lint, isort), mkdocs (docs), tox (optional/matrix), wheel/build + +Codebase Structure (high level) +- src/setuptools_scm/: library code + - _cli.py, __main__.py: CLI entry (`python -m setuptools_scm`, `setuptools-scm`) + - git.py, hg.py, hg_git.py: VCS parsing + - _file_finders/: discover files for sdist + - _integration/: setuptools and pyproject integration + - version.py and helpers: version schemes/local version logic + - discover.py, fallbacks.py: inference and archival fallbacks +- testing/: pytest suite and fixtures +- docs/: mkdocs documentation +- pyproject.toml: project metadata, pytest and ruff config +- tox.ini: alternate CI/matrix, flake8 defaults +- uv.lock: locked dependencies + +Conventions +- Use uv to run commands (`uv run ...`); tests live under `testing/` per pytest config. +- Type hints throughout; strict mypy enforced; ruff governs lint rules and import layout (isort in ruff). diff --git a/.serena/memories/style_and_conventions.md b/.serena/memories/style_and_conventions.md new file mode 100644 index 00000000..aec4e917 --- /dev/null +++ b/.serena/memories/style_and_conventions.md @@ -0,0 +1,17 @@ +Style and Conventions + +- Typing + - mypy strict is enabled; add precise type hints for public functions/classes. + - Prefer explicit/clear types; avoid `Any` and unsafe casts. +- Linting/Imports + - Ruff is the canonical linter (config in pyproject). Respect its rules and isort settings (single-line imports, ordered, types grouped). + - Flake8 config exists in tox.ini but ruff linting is primary. +- Formatting + - Follow ruff guidance; keep lines <= 88 where applicable (flake8 reference). +- Testing + - Pytest with `testing/` as testpath; default 5m timeout; warnings treated as errors. + - Use existing fixtures; add `@pytest.mark` markers if needed (see pyproject markers). +- Logging + - Tests run with log level info/debug; avoid noisy logs in normal library code. +- General + - Small, focused functions; early returns; explicit errors. Keep APIs documented with concise docstrings. diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md new file mode 100644 index 00000000..8eeeab96 --- /dev/null +++ b/.serena/memories/suggested_commands.md @@ -0,0 +1,30 @@ +Environment +- Install deps (uses default groups test, docs): + - uv sync + +Core Dev +- Run tests: + - uv run pytest +- Lint (ruff): + - uv run ruff check . + - uv run ruff check . --fix # optional autofix +- Type check (mypy strict): + - uv run mypy +- Build docs: + - uv run mkdocs serve --dev-addr localhost:8000 + - uv run mkdocs build --clean --strict + +Entrypoints / Tooling +- CLI version/debug: + - uv run python -m setuptools_scm --help + - uv run python -m setuptools_scm + - uv run setuptools-scm --help +- Build dist and verify: + - uv run python -m build + - uv run twine check dist/* +- Optional matrix via tox: + - uv run tox -q + +Git/Linux Utilities (Linux host) +- git status / git log --oneline --graph --decorate +- ls -la; find . -name "pattern"; grep -R "text" . diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 00000000..505274b8 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,68 @@ +# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) +# * For C, use cpp +# * For JavaScript, use typescript +# Special requirements: +# * csharp: Requires the presence of a .sln file in the project folder. +language: python + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed)on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "setuptools_scm" diff --git a/CHANGELOG.md b/CHANGELOG.md index d96624ca..6b636da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Unreleased + +### Added + +- add simplified activation via `setuptools-scm[simple]` extra + + A new streamlined way to enable version inference without requiring a `[tool.setuptools_scm]` section. + When `setuptools-scm[simple]` is in `build-system.requires` and `version` is in `project.dynamic`, + version inference is automatically enabled with default settings. + + +### removed + +- unchecked simplified activation - too many projects use setups where it would fail + + ## v9.1.1 ### fixed diff --git a/docs/config.md b/docs/config.md index 80ad24fd..83d11e2b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2,11 +2,25 @@ ## When is configuration needed? -Starting with setuptools-scm 8.1+, explicit configuration is **optional** in many cases: +setuptools-scm provides flexible activation options: -- **No configuration needed**: If `setuptools_scm` (or `setuptools-scm`) is in your `build-system.requires`, setuptools-scm will automatically activate with sensible defaults. +### Simplified Activation (No Configuration Needed) -- **Configuration recommended**: Use the `[tool.setuptools_scm]` section when you need to: +For basic usage, use the `simple` extra with no configuration: + +```toml title="pyproject.toml" +[build-system] +requires = ["setuptools>=80", "setuptools-scm[simple]>=8"] + +[project] +dynamic = ["version"] +``` + +This automatically enables version inference with default settings. + +### Explicit Configuration (Full Control) + +Use the `[tool.setuptools_scm]` section when you need to: - Write version files (`version_file`) - Customize version schemes (`version_scheme`, `local_scheme`) - Set custom tag patterns (`tag_regex`) diff --git a/docs/index.md b/docs/index.md index 303017f3..c86f93ce 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,10 +30,11 @@ Note: `setuptools-scm>=8` intentionally doesn't depend on setuptools to ease non Please ensure a recent version of setuptools is installed (minimum: >=61, recommended: >=80 for best compatibility). Support for setuptools <80 is deprecated and will be removed in a future release. +**Simplified setup (recommended for basic usage):** ```toml title="pyproject.toml" [build-system] -requires = ["setuptools>=80", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm[simple]>=8"] build-backend = "setuptools.build_meta" [project] @@ -41,10 +42,24 @@ name = "example" # Important: Remove any existing version declaration # version = "0.0.1" dynamic = ["version"] -# more missing + +# No additional configuration needed! +``` + +**With custom configuration:** + +```toml title="pyproject.toml" +[build-system] +requires = ["setuptools>=80", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "example" +dynamic = ["version"] [tool.setuptools_scm] -´´´ +# Custom configuration options go here +``` !!! tip "Recommended Tag Format" diff --git a/docs/usage.md b/docs/usage.md index 28e0bc52..53f70445 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -7,30 +7,40 @@ Support for setuptools <80 is deprecated and will be removed in a future release. The examples below use `setuptools>=80` as the recommended version. -There are two ways to configure `setuptools-scm` at build time, depending on your needs: +There are three ways to enable `setuptools-scm` at build time: -### Automatic Configuration (Recommended for Simple Cases) +### Simplified Activation (new) -For projects that don't need custom configuration, simply include `setuptools-scm` -in your build requirements: +For basic usage without custom configuration, use the `simple` extra: ```toml title="pyproject.toml" [build-system] -requires = ["setuptools>=80", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm[simple]>=8"] build-backend = "setuptools.build_meta" [project] # version = "0.0.1" # Remove any existing version parameter. dynamic = ["version"] + +# No [tool.setuptools_scm] section needed for basic usage! ``` -**That's it!** Starting with setuptools-scm 8.1+, if `setuptools_scm` (or `setuptools-scm`) -is present in your `build-system.requires`, setuptools-scm will automatically activate -with default settings. +This streamlined approach automatically enables version inference when: +- `setuptools-scm[simple]` is listed in `build-system.requires` +- `version` is included in `project.dynamic` + +!!! tip "When to use simplified activation" -### Explicit Configuration + Use simplified activation when you: + - Want basic SCM version inference with default settings + - Don't need custom version schemes or file writing + - Prefer minimal configuration -If you need to customize setuptools-scm behavior, use the `tool.setuptools_scm` section: + Upgrade to explicit configuration if you need customization. + +### Explicit Configuration (full control) + +Add a `tool.setuptools_scm` section for custom configuration: ```toml title="pyproject.toml" [build-system] @@ -51,20 +61,25 @@ pre_parse = "fail_on_missing_submodules" # Fail if submodules are not initializ describe_command = "git describe --dirty --tags --long --exclude *js*" # Custom describe command ``` -Both approaches will work with projects that support PEP 518 ([pip](https://pypi.org/project/pip) and -[pep517](https://pypi.org/project/pep517/)). -Tools that still invoke `setup.py` must ensure build requirements are installed +Projects must support PEP 518 ([pip](https://pypi.org/project/pip) and +[pep517](https://pypi.org/project/pep517/)). Tools that still invoke `setup.py` +must ensure build requirements are installed. + +### Using the setup keyword -!!! info "How Automatic Detection Works" +Alternatively, enable `setuptools-scm` via the `use_scm_version` keyword in `setup.py`. +This also counts as an explicit opt-in and does not require a tool section. - When setuptools-scm is listed in `build-system.requires`, it automatically detects this during the build process and activates with default settings. This means: +!!! note "Legacy simplified activation" - - ✅ **Automatic activation**: No `[tool.setuptools_scm]` section needed - - ✅ **Default behavior**: Uses standard version schemes and SCM detection - - ✅ **Error handling**: Provides helpful error messages if configuration is missing - - ⚙️ **Customization**: Add `[tool.setuptools_scm]` section when you need custom options + Previous versions had a "simplified" activation where listing `setuptools_scm` + in `build-system.requires` together with `project.dynamic = ["version"]` would + auto-enable version inference. This behavior was removed due to regressions and + ambiguous activation. - Both package names are detected: `setuptools_scm` and `setuptools-scm` (with dash). + The new simplified activation using the `[simple]` extra provides the same + convenience but with explicit opt-in, making it clear when version inference + should be enabled. ### Version files diff --git a/pyproject.toml b/pyproject.toml index 78b8f437..d6005cd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ dependencies = [ ] [project.optional-dependencies] rich = ["rich"] +simple = [] toml = [] [dependency-groups] @@ -67,6 +68,8 @@ test = [ "pytest", "pytest-timeout", # Timeout protection for CI/CD "rich", + "ruff", + "mypy~=1.13.0", # pinned to old for python 3.8 'typing-extensions; python_version < "3.11"', "wheel", "griffe", diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 7c1d185a..81a78e84 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -270,33 +270,22 @@ def from_file( cls, name: str | os.PathLike[str] = "pyproject.toml", dist_name: str | None = None, - missing_file_ok: bool = False, - missing_section_ok: bool = False, pyproject_data: PyProjectData | None = None, **kwargs: Any, ) -> Configuration: """ Read Configuration from pyproject.toml (or similar). Raises exceptions when file is not found or toml is - not installed or the file has invalid format or does - not contain setuptools_scm configuration (either via - _ [tool.setuptools_scm] section or build-system.requires). + not installed or the file has invalid format. Parameters: - name: path to pyproject.toml - dist_name: name of the distribution - - missing_file_ok: if True, do not raise an error if the file is not found - - missing_section_ok: if True, do not raise an error if the section is not found - (workaround for not walking the dependency graph when figuring out if setuptools_scm is a dependency) - **kwargs: additional keyword arguments to pass to the Configuration constructor """ if pyproject_data is None: - pyproject_data = _read_pyproject( - Path(name), - missing_section_ok=missing_section_ok, - missing_file_ok=missing_file_ok, - ) + pyproject_data = _read_pyproject(Path(name)) args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs) args.update(read_toml_overrides(args["dist_name"])) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index df5d30c8..f041484f 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -7,8 +7,10 @@ from typing import Sequence from .. import _log +from .. import _types as _t from .._requirement_cls import extract_package_name from .toml import TOML_RESULT +from .toml import InvalidTomlError from .toml import read_toml_content log = _log.log.getChild("pyproject_reading") @@ -16,6 +18,10 @@ _ROOT = "root" +DEFAULT_PYPROJECT_PATH = Path("pyproject.toml") +DEFAULT_TOOL_NAME = "setuptools_scm" + + @dataclass class PyProjectData: path: Path @@ -25,51 +31,104 @@ class PyProjectData: is_required: bool section_present: bool project_present: bool + build_requires: list[str] @classmethod def for_testing( cls, + *, is_required: bool = False, section_present: bool = False, project_present: bool = False, project_name: str | None = None, + has_dynamic_version: bool = True, + build_requires: list[str] | None = None, + local_scheme: str | None = None, ) -> PyProjectData: """Create a PyProjectData instance for testing purposes.""" + project: TOML_RESULT if project_name is not None: project = {"name": project_name} assert project_present else: project = {} + + # If project is present and has_dynamic_version is True, add dynamic=['version'] + if project_present and has_dynamic_version: + project["dynamic"] = ["version"] + + if build_requires is None: + build_requires = [] + if local_scheme is not None: + assert section_present + section = {"local_scheme": local_scheme} + else: + section = {} return cls( - path=Path("pyproject.toml"), - tool_name="setuptools_scm", + path=DEFAULT_PYPROJECT_PATH, + tool_name=DEFAULT_TOOL_NAME, project=project, - section={}, + section=section, is_required=is_required, section_present=section_present, project_present=project_present, + build_requires=build_requires, + ) + + @classmethod + def empty( + cls, path: Path = DEFAULT_PYPROJECT_PATH, tool_name: str = DEFAULT_TOOL_NAME + ) -> PyProjectData: + return cls( + path=path, + tool_name=tool_name, + project={}, + section={}, + is_required=False, + section_present=False, + project_present=False, + build_requires=[], ) @property def project_name(self) -> str | None: return self.project.get("name") - def verify_dynamic_version_when_required(self) -> None: - """Verify that dynamic=['version'] is set when setuptools-scm is used as build dependency indicator.""" - if self.is_required and not self.section_present: - # When setuptools-scm is in build-system.requires but no tool section exists, - # we need to verify that dynamic=['version'] is set in the project section - # But only if there's actually a project section - if not self.project_present: - # No project section, so don't auto-activate setuptools_scm - return - dynamic = self.project.get("dynamic", []) - if "version" not in dynamic: - raise ValueError( - f"{self.path}: setuptools-scm is present in [build-system].requires " - f"but dynamic=['version'] is not set in [project]. " - f"Either add dynamic=['version'] to [project] or add a [tool.{self.tool_name}] section." - ) + @property + def project_version(self) -> str | None: + """Return the static version from [project] if present. + + When the project declares dynamic = ["version"], the version + is intentionally omitted from [project] and this returns None. + """ + return self.project.get("version") + + def should_infer(self) -> bool: + """ + Determine if setuptools_scm should infer version based on configuration. + + Infer when: + 1. An explicit [tool.setuptools_scm] section is present, OR + 2. setuptools-scm[simple] is in build-system.requires AND + version is in project.dynamic + + Returns: + True if [tool.setuptools_scm] is present, otherwise False + """ + # Original behavior: explicit tool section + if self.section_present: + return True + + # New behavior: simple extra + dynamic version + if self.project_present: + dynamic_fields = self.project.get("dynamic", []) + if "version" in dynamic_fields: + if has_build_package_with_extra( + self.build_requires, "setuptools-scm", "simple" + ): + return True + + return False def has_build_package( @@ -82,61 +141,89 @@ def has_build_package( return False +def has_build_package_with_extra( + requires: Sequence[str], canonical_build_package_name: str, extra_name: str +) -> bool: + """Check if a build dependency has a specific extra. + + Args: + requires: List of requirement strings from build-system.requires + canonical_build_package_name: The canonical package name to look for + extra_name: The extra name to check for (e.g., "simple") + + Returns: + True if the package is found with the specified extra + """ + from .._requirement_cls import Requirement + + for requirement_string in requires: + try: + requirement = Requirement(requirement_string) + package_name = extract_package_name(requirement_string) + if package_name == canonical_build_package_name: + if extra_name in requirement.extras: + return True + except Exception: + # If parsing fails, continue to next requirement + continue + return False + + def read_pyproject( - path: Path = Path("pyproject.toml"), - tool_name: str = "setuptools_scm", + path: Path = DEFAULT_PYPROJECT_PATH, + tool_name: str = DEFAULT_TOOL_NAME, canonical_build_package_name: str = "setuptools-scm", - missing_section_ok: bool = False, - missing_file_ok: bool = False, + _given_result: _t.GivenPyProjectResult = None, ) -> PyProjectData: - try: - defn = read_toml_content(path) - except FileNotFoundError: - if missing_file_ok: - log.warning("File %s not found, using empty configuration", path) - return PyProjectData( - path=path, - tool_name=tool_name, - project={}, - section={}, - is_required=False, - section_present=False, - project_present=False, - ) - else: - raise + """Read and parse pyproject configuration. + + This function supports dependency injection for tests via `_given_result`. + + Parameters: + - path: Path to the pyproject file + - tool_name: The tool section name (default: `setuptools_scm`) + - canonical_build_package_name: Normalized build requirement name + - _given_result: Optional testing hook. Can be: + - PyProjectData: returned directly + - InvalidTomlError | FileNotFoundError: raised directly + - None: read from filesystem + """ + + if _given_result is not None: + if isinstance(_given_result, PyProjectData): + return _given_result + if isinstance(_given_result, (InvalidTomlError, FileNotFoundError)): + raise _given_result + + defn = read_toml_content(path) requires: list[str] = defn.get("build-system", {}).get("requires", []) is_required = has_build_package(requires, canonical_build_package_name) - try: - section = defn.get("tool", {})[tool_name] - section_present = True - except LookupError as e: - if not is_required and not missing_section_ok: - # Enhanced error message that mentions both configuration options - error = ( - f"{path} does not contain a tool.{tool_name} section. " - f"setuptools_scm requires configuration via either:\n" - f" 1. [tool.{tool_name}] section in {path}, or\n" - f" 2. {tool_name} (or setuptools-scm) in [build-system] requires" - ) - raise LookupError(error) from e - else: - error = f"{path} does not contain a tool.{tool_name} section" - log.warning("toml section missing %r", error, exc_info=True) - section = {} - section_present = False + tool_section = defn.get("tool", {}) + section = tool_section.get(tool_name, {}) + section_present = tool_name in tool_section + + if not section_present: + log.warning( + "toml section missing %r does not contain a tool.%s section", + path, + tool_name, + ) project = defn.get("project", {}) project_present = "project" in defn pyproject_data = PyProjectData( - path, tool_name, project, section, is_required, section_present, project_present + path, + tool_name, + project, + section, + is_required, + section_present, + project_present, + requires, ) - # Verify dynamic version when setuptools-scm is used as build dependency indicator - pyproject_data.verify_dynamic_version_when_required() - return pyproject_data diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/src/setuptools_scm/_integration/setup_cfg.py index e904d7d1..4e485600 100644 --- a/src/setuptools_scm/_integration/setup_cfg.py +++ b/src/setuptools_scm/_integration/setup_cfg.py @@ -2,20 +2,40 @@ import os +from dataclasses import dataclass +from pathlib import Path + import setuptools -def read_dist_name_from_setup_cfg( - input: str | os.PathLike[str] = "setup.cfg", -) -> str | None: - # minimal effort to read dist_name off setup.cfg metadata +@dataclass +class SetuptoolsBasicData: + path: Path + name: str | None + version: str | None + + +def read_setup_cfg(input: str | os.PathLike[str] = "setup.cfg") -> SetuptoolsBasicData: + """Parse setup.cfg and return unified data. Does not raise if file is missing.""" import configparser + path = Path(input) parser = configparser.ConfigParser() parser.read([input], encoding="utf-8") - dist_name = parser.get("metadata", "name", fallback=None) - return dist_name + + name = parser.get("metadata", "name", fallback=None) + version = parser.get("metadata", "version", fallback=None) + return SetuptoolsBasicData(path=path, name=name, version=version) -def _dist_name_from_legacy(dist: setuptools.Distribution) -> str | None: - return dist.metadata.name or read_dist_name_from_setup_cfg() +def extract_from_legacy( + dist: setuptools.Distribution, + *, + _given_legacy_data: SetuptoolsBasicData | None = None, +) -> SetuptoolsBasicData: + base = _given_legacy_data if _given_legacy_data is not None else read_setup_cfg() + if base.name is None: + base.name = dist.metadata.name + if base.version is None: + base.version = dist.metadata.version + return base diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 9c9733e7..aa1c645a 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -8,8 +8,12 @@ import setuptools +from .. import _types as _t +from .pyproject_reading import PyProjectData from .pyproject_reading import read_pyproject -from .setup_cfg import _dist_name_from_legacy +from .setup_cfg import SetuptoolsBasicData +from .setup_cfg import extract_from_legacy +from .toml import InvalidTomlError from .version_inference import get_version_inference_config log = logging.getLogger(__name__) @@ -64,6 +68,10 @@ def version_keyword( dist: setuptools.Distribution, keyword: str, value: bool | dict[str, Any] | Callable[[], dict[str, Any]], + *, + _given_pyproject_data: _t.GivenPyProjectResult = None, + _given_legacy_data: SetuptoolsBasicData | None = None, + _get_version_inference_config: _t.GetVersionInferenceConfig = get_version_inference_config, ) -> None: """apply version infernce when setup(use_scm_version=...) is used this takes priority over the finalize_options based version @@ -78,29 +86,49 @@ def version_keyword( "dist_name may not be specified in the setup keyword " ) - dist_name: str | None = _dist_name_from_legacy(dist) + legacy_data = extract_from_legacy(dist, _given_legacy_data=_given_legacy_data) + dist_name: str | None = legacy_data.name was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) - # Get pyproject data + # Exit early if overrides is empty dict AND version was set by infer + if overrides == {} and was_set_by_infer: + return + + # Get pyproject data (support direct injection for tests) try: - pyproject_data = read_pyproject(missing_section_ok=True, missing_file_ok=True) - except (LookupError, ValueError) as e: + pyproject_data = read_pyproject(_given_result=_given_pyproject_data) + except FileNotFoundError: + log.debug("pyproject.toml not found, proceeding with empty configuration") + pyproject_data = PyProjectData.empty() + except InvalidTomlError as e: log.debug("Configuration issue in pyproject.toml: %s", e) return - result = get_version_inference_config( + # Pass None as current_version if overrides is truthy AND version was set by infer + current_version = ( + None + if (overrides and was_set_by_infer) + else (legacy_data.version or pyproject_data.project_version) + ) + + result = _get_version_inference_config( dist_name=dist_name, - current_version=dist.metadata.version, + current_version=current_version, pyproject_data=pyproject_data, overrides=overrides, - was_set_by_infer=was_set_by_infer, ) result.apply(dist) -def infer_version(dist: setuptools.Distribution) -> None: +def infer_version( + dist: setuptools.Distribution, + *, + _given_pyproject_data: _t.GivenPyProjectResult = None, + _given_legacy_data: SetuptoolsBasicData | None = None, + _get_version_inference_config: _t.GetVersionInferenceConfig = get_version_inference_config, +) -> None: """apply version inference from the finalize_options hook this is the default for pyproject.toml based projects that don't use the use_scm_version keyword @@ -110,20 +138,22 @@ def infer_version(dist: setuptools.Distribution) -> None: _log_hookstart("infer_version", dist) - dist_name = _dist_name_from_legacy(dist) + legacy_data = extract_from_legacy(dist, _given_legacy_data=_given_legacy_data) + dist_name = legacy_data.name try: - pyproject_data = read_pyproject(missing_section_ok=True) + pyproject_data = read_pyproject(_given_result=_given_pyproject_data) except FileNotFoundError: log.debug("pyproject.toml not found, skipping infer_version") return - except (LookupError, ValueError) as e: + except InvalidTomlError as e: log.debug("Configuration issue in pyproject.toml: %s", e) return - result = get_version_inference_config( + # Only infer when tool section present per get_version_inference_config + result = _get_version_inference_config( dist_name=dist_name, - current_version=dist.metadata.version, + current_version=legacy_data.version or pyproject_data.project_version, pyproject_data=pyproject_data, ) result.apply(dist) diff --git a/src/setuptools_scm/_integration/toml.py b/src/setuptools_scm/_integration/toml.py index 8ca38d97..2253287c 100644 --- a/src/setuptools_scm/_integration/toml.py +++ b/src/setuptools_scm/_integration/toml.py @@ -29,6 +29,10 @@ TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT] +class InvalidTomlError(ValueError): + """Raised when TOML data cannot be parsed.""" + + def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RESULT: try: data = path.read_text(encoding="utf-8") @@ -39,7 +43,10 @@ def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RE log.debug("%s missing, presuming default %r", path, default) return default else: - return load_toml(data) + try: + return load_toml(data) + except Exception as e: # tomllib/tomli raise different decode errors + raise InvalidTomlError(f"Invalid TOML in {path}") from e class _CheatTomlData(TypedDict): @@ -52,8 +59,11 @@ def load_toml_or_inline_map(data: str | None) -> dict[str, Any]: """ if not data: return {} - elif data[0] == "{": - data = "cheat=" + data - loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data)) - return loaded["cheat"] - return load_toml(data) + try: + if data[0] == "{": + data = "cheat=" + data + loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data)) + return loaded["cheat"] + return load_toml(data) + except Exception as e: # tomllib/tomli raise different decode errors + raise InvalidTomlError("Invalid TOML content") from e diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index 51f42cd5..6258d90b 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -5,6 +5,8 @@ from typing import Any from typing import Union +from setuptools import Distribution + from .. import _log if TYPE_CHECKING: @@ -21,87 +23,91 @@ class VersionInferenceConfig: pyproject_data: PyProjectData | None overrides: dict[str, Any] | None - def apply(self, dist: Any) -> None: + def apply(self, dist: Distribution) -> None: """Apply version inference to the distribution.""" - from .. import _config as _config_module - from .._get_version_impl import _get_version - from .._get_version_impl import _version_missing - - # Clear version if it was set by infer_version (overrides is None means infer_version context) - # OR if we have overrides (version_keyword context) and the version was set by infer_version - was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) - if was_set_by_infer and (self.overrides is None or self.overrides): - dist._setuptools_scm_version_set_by_infer = False - dist.metadata.version = None - - config = _config_module.Configuration.from_file( - dist_name=self.dist_name, - pyproject_data=self.pyproject_data, - missing_file_ok=True, - missing_section_ok=True, - **(self.overrides or {}), + version_string = infer_version_string( + self.dist_name, + self.pyproject_data, # type: ignore[arg-type] + self.overrides, + force_write_version_files=True, ) - - # Get and assign version - maybe_version = _get_version(config, force_write_version_files=True) - if maybe_version is None: - _version_missing(config) - else: - assert dist.metadata.version is None - dist.metadata.version = maybe_version + dist.metadata.version = version_string # Mark that this version was set by infer_version if overrides is None (infer_version context) if self.overrides is None: - dist._setuptools_scm_version_set_by_infer = True + dist._setuptools_scm_version_set_by_infer = True # type: ignore[attr-defined] @dataclass -class VersionInferenceError: +class VersionInferenceWarning: """Error message for user.""" message: str - should_warn: bool = False - def apply(self, dist: Any) -> None: + def apply(self, dist: Distribution) -> None: """Apply error handling to the distribution.""" import warnings - if self.should_warn: - warnings.warn(self.message) - - -@dataclass -class VersionInferenceException: - """Exception that should be raised.""" - - exception: Exception - - def apply(self, dist: Any) -> None: - """Apply exception handling to the distribution.""" - raise self.exception + warnings.warn(self.message) +@dataclass(frozen=True) class VersionInferenceNoOp: """No operation result - silent skip.""" - def apply(self, dist: Any) -> None: + def apply(self, dist: Distribution) -> None: """Apply no-op to the distribution.""" VersionInferenceResult = Union[ VersionInferenceConfig, # Proceed with inference - VersionInferenceError, # Show error/warning - VersionInferenceException, # Raise exception + VersionInferenceWarning, # Show warning VersionInferenceNoOp, # Don't infer (silent) ] +def infer_version_string( + dist_name: str | None, + pyproject_data: PyProjectData, + overrides: dict[str, Any] | None = None, + *, + force_write_version_files: bool = False, +) -> str: + """ + Compute the inferred version string from the given inputs without requiring a + setuptools Distribution instance. This is a pure helper that simplifies + integration tests by avoiding file I/O and side effects on a Distribution. + + Parameters: + dist_name: Optional distribution name (used for overrides and env scoping) + pyproject_data: Parsed PyProjectData (may be constructed via for_testing()) + overrides: Optional override configuration (same keys as [tool.setuptools_scm]) + force_write_version_files: When True, apply write_to/version_file effects + + Returns: + The computed version string. + """ + from .. import _config as _config_module + from .._get_version_impl import _get_version + from .._get_version_impl import _version_missing + + config = _config_module.Configuration.from_file( + dist_name=dist_name, pyproject_data=pyproject_data, **(overrides or {}) + ) + + maybe_version = _get_version( + config, force_write_version_files=force_write_version_files + ) + if maybe_version is None: + _version_missing(config) + return maybe_version + + def get_version_inference_config( dist_name: str | None, current_version: str | None, pyproject_data: PyProjectData, overrides: dict[str, Any] | None = None, - was_set_by_infer: bool = False, ) -> VersionInferenceResult: """ Determine whether and how to perform version inference. @@ -111,72 +117,25 @@ def get_version_inference_config( current_version: Current version if any pyproject_data: PyProjectData from parser (None if file doesn't exist) overrides: Override configuration (None for no overrides) - was_set_by_infer: Whether current version was set by infer_version Returns: VersionInferenceResult with the decision and configuration """ - if dist_name is None: - dist_name = pyproject_data.project_name - - # Handle version already set - if current_version is not None: - if was_set_by_infer: - if overrides is not None and overrides: - # Clear version and proceed with actual overrides (non-empty dict) - return VersionInferenceConfig( - dist_name=dist_name, - pyproject_data=pyproject_data, - overrides=overrides, - ) - else: - # Keep existing version from infer_version (no overrides or empty overrides) - # But allow re-inferring if this is another infer_version call - if overrides is None: - # This is another infer_version call, allow it to proceed - return VersionInferenceConfig( - dist_name=dist_name, - pyproject_data=pyproject_data, - overrides=overrides, - ) - else: - # This is version_keyword with empty overrides, keep existing version - return VersionInferenceNoOp() - else: - # Version set by something else - return VersionInferenceError( - f"version of {dist_name} already set", should_warn=True - ) - - # Handle setuptools-scm package - if dist_name == "setuptools-scm": - return VersionInferenceNoOp() - - # Handle missing configuration - if not pyproject_data.is_required and not pyproject_data.section_present: - # If version_keyword was called (overrides is not None), activate setuptools_scm - # This handles both use_scm_version=True (empty {}) and use_scm_version={config} - if overrides is not None: - return VersionInferenceConfig( - dist_name=dist_name, - pyproject_data=pyproject_data, - overrides=overrides, - ) - # If infer_version was called (overrides is None), only activate with config - return VersionInferenceNoOp() - # Handle missing project section when required - if ( - pyproject_data.is_required - and not pyproject_data.section_present - and not pyproject_data.project_present - and overrides is None # Only return NoOp for infer_version, not version_keyword - ): - return VersionInferenceNoOp() - - # All conditions met - proceed with inference - return VersionInferenceConfig( + config = VersionInferenceConfig( dist_name=dist_name, pyproject_data=pyproject_data, overrides=overrides, ) + + inference_implied = pyproject_data.should_infer() or overrides is not None + + if inference_implied: + if current_version is None: + return config + else: + return VersionInferenceWarning( + f"version of {dist_name} already set", + ) + else: + return VersionInferenceNoOp() diff --git a/src/setuptools_scm/_requirement_cls.py b/src/setuptools_scm/_requirement_cls.py index 810e91fa..9bb88462 100644 --- a/src/setuptools_scm/_requirement_cls.py +++ b/src/setuptools_scm/_requirement_cls.py @@ -1,5 +1,7 @@ from __future__ import annotations +__all__ = ["Requirement", "extract_package_name"] + try: from packaging.requirements import Requirement from packaging.utils import canonicalize_name diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index 6cc4e774..4f8874fb 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -5,10 +5,13 @@ from typing import TYPE_CHECKING from typing import Callable from typing import List +from typing import Protocol from typing import Sequence from typing import Tuple from typing import Union +from setuptools import Distribution + if TYPE_CHECKING: import sys @@ -18,6 +21,8 @@ from typing_extensions import TypeAlias from . import version + from ._integration.pyproject_reading import PyProjectData + from ._integration.toml import InvalidTomlError PathT: TypeAlias = Union["os.PathLike[str]", str] @@ -29,3 +34,28 @@ # Git pre-parse function types GIT_PRE_PARSE: TypeAlias = Union[str, None] + +# Testing injection types for configuration reading +GivenPyProjectResult: TypeAlias = Union[ + "PyProjectData", "InvalidTomlError", FileNotFoundError, None +] + + +class VersionInferenceApplicable(Protocol): + """A result object from version inference decision that can be applied to a dist.""" + + def apply(self, dist: Distribution) -> None: # pragma: no cover - structural type + ... + + +class GetVersionInferenceConfig(Protocol): + """Callable protocol for the decision function used by integration points.""" + + def __call__( + self, + dist_name: str | None, + current_version: str | None, + pyproject_data: PyProjectData, + overrides: dict[str, object] | None = None, + ) -> VersionInferenceApplicable: # pragma: no cover - structural type + ... diff --git a/testing/INTEGRATION_MIGRATION_PLAN.md b/testing/INTEGRATION_MIGRATION_PLAN.md new file mode 100644 index 00000000..432a05b1 --- /dev/null +++ b/testing/INTEGRATION_MIGRATION_PLAN.md @@ -0,0 +1,92 @@ +## Setuptools integration test migration plan + +Purpose: streamline/simplify integration codepaths and make tests faster and easier to write by preferring unit-level inference over setuptools-driven E2E where possible. + +Reference helper for unit tests: + +```python +from setuptools_scm._integration.pyproject_reading import PyProjectData +from setuptools_scm._integration.version_inference import infer_version_string + +version = infer_version_string( + dist_name="pkg", + pyproject_data=PyProjectData.for_testing(project_present=True, section_present=True, project_name="pkg"), + overrides={"fallback_version": "1.2.3"}, +) +``` + +### Completed +- [x] Introduced `infer_version_string` pure helper to compute versions without a `Distribution` or `setup.py`. + +### Migration candidates (replace E2E/Distribution-hook tests with unit inference) +- [ ] `testing/test_integration.py::test_pyproject_support` + - Proposed unit: `test_infer_fallback_version_from_pyproject` + - Notes: Use `PyProjectData.for_testing(..., section_present=True, project_present=True)` + overrides `{fallback_version: "12.34"}`. + +- [ ] `testing/test_integration.py::test_setuptools_version_keyword_ensures_regex` + - Proposed unit: `test_infer_tag_regex_from_overrides` + - Notes: Create repo/tag in `wd`, call `infer_version_string(..., overrides={"tag_regex": "(1.0)"})`. + +- [ ] `testing/test_basic_api.py::test_parentdir_prefix` + - Proposed unit: `test_infer_parentdir_prefix_version` + - Notes: Use directory name prefix and `{parentdir_prefix_version: "projectname-"}`. + +- [ ] `testing/test_basic_api.py::test_fallback` + - Proposed unit: `test_infer_fallback_version` + - Notes: `{fallback_version: "12.34"}`. + +- [ ] `testing/test_basic_api.py::test_empty_pretend_version` + - Proposed unit: `test_infer_with_empty_pretend_uses_fallback` + - Notes: Set `SETUPTOOLS_SCM_PRETEND_VERSION=""`, infer with fallback. + +- [ ] `testing/test_basic_api.py::test_empty_pretend_version_named` + - Proposed unit: `test_infer_with_empty_named_pretend_uses_fallback` + - Notes: Use named pretend env var and fallback. + +- [ ] `testing/test_regressions.py::test_use_scm_version_callable` + - Proposed unit: `test_infer_with_callable_version_scheme` + - Notes: Pass callable via `overrides={"version_scheme": callable}` to `infer_version_string`. + +- [ ] `testing/test_git.py::test_root_relative_to` + - Proposed unit: `test_configuration_absolute_root_resolution` + - Notes: Assert `Configuration.absolute_root` behavior or use `Configuration.from_data(..., root/relative_to)`; avoid `setup.py`. + +- [ ] `testing/test_git.py::test_root_search_parent_directories` + - Proposed unit: `test_configuration_search_parent_directories` + - Notes: Prefer `Configuration(search_parent_directories=True)` + direct `_get_version` or `infer_version_string`. + +### Tests to keep as integration/E2E +- `testing/test_integration.py::test_integration_function_call_order` + - Validates precedence/ordering between `infer_version` and `version_keyword` hooks on `Distribution`. + +- `testing/test_integration.py::test_distribution_provides_extras` + - Verifies installed distribution metadata (extras exposure). + +- `testing/test_integration.py::test_git_archival_plugin_ignored` + - Entry point filtering behavior. + +- `testing/test_git.py::test_git_version_unnormalized_setuptools` (parameterized) + - Asserts difference between file write (`write_to` non-normalized) vs setuptools-normalized dist metadata. Requires setuptools behavior; not reproducible by pure helper. + +- Maintain a minimal smoke test to ensure `setup.py --version` works end-to-end (one per major path). + +### Already covered by unit-level decision tests (no action) +- `testing/test_version_inference.py` suite + - Exercises `get_version_inference_config` across configuration matrices using `PyProjectData.for_testing`. + +### New unit tests to add (pure inference) +- [ ] `test_infer_local_scheme_no_local_version` + - Use `PyProjectData.for_testing(section_present=True, project_present=True, local_scheme="no-local-version")`. + +- [ ] `test_infer_with_env_pretend_version_and_metadata` + - Set pretend version + metadata env vars; assert combined result via `infer_version_string`. + +- [ ] `test_infer_respects_nested_scm_git_config` + - Provide nested TOML-equivalent via `overrides={"scm": {"git": {"pre_parse": "fail_on_missing_submodules"}}}`. + +### Notes and pitfalls +- Some behaviors are specific to setuptools (normalization of dist metadata vs written file contents) and should remain integration tests. +- Prefer `PyProjectData.for_testing(...)` to avoid file I/O in new unit tests. +- For tests that assert version-file writing, call `infer_version_string(..., force_write_version_files=True)` and set `write_to`/`version_file` in overrides. + + diff --git a/testing/conftest.py b/testing/conftest.py index ec936f7c..de1d9900 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -38,7 +38,15 @@ def pytest_report_header() -> list[str]: for pkg in VERSION_PKGS: pkg_version = version(pkg) path = __import__(pkg).__file__ - res.append(f"{pkg} version {pkg_version} from {path!r}") + if path and "site-packages" in path: + # Replace everything up to and including site-packages with site:: + parts = path.split("site-packages", 1) + if len(parts) > 1: + path = "site:." + parts[1] + elif path and str(Path.cwd()) in path: + # Replace current working directory with CWD:: + path = path.replace(str(Path.cwd()), "CWD:.") + res.append(f"{pkg} version {pkg_version} from {path}") return res diff --git a/testing/test_integration.py b/testing/test_integration.py index 467cbdf7..be6e3cfe 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -2,7 +2,6 @@ import importlib.metadata import logging -import os import re import subprocess import sys @@ -17,6 +16,8 @@ from packaging.version import Version from setuptools_scm._integration import setuptools as setuptools_integration +from setuptools_scm._integration.pyproject_reading import PyProjectData +from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData from setuptools_scm._requirement_cls import extract_package_name if TYPE_CHECKING: @@ -44,8 +45,6 @@ def wd(wd: WorkDir) -> WorkDir: def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - if sys.version_info < (3, 11): - pytest.importorskip("tomli") monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") pkg = tmp_path / "package" pkg.mkdir() @@ -70,200 +69,6 @@ def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> N assert res.stdout == "12.34" -PYPROJECT_FILES = { - "setup.py": "[tool.setuptools_scm]\n", - "setup.cfg": "[tool.setuptools_scm]\n", - "pyproject tool.setuptools_scm": ( - "[project]\nname='setuptools_scm_example'\n[tool.setuptools_scm]" - ), - "pyproject.project": ( - "[project]\nname='setuptools_scm_example'\n" - "dynamic=['version']\n[tool.setuptools_scm]" - ), -} - -SETUP_PY_PLAIN = "__import__('setuptools').setup()" -SETUP_PY_WITH_NAME = "__import__('setuptools').setup(name='setuptools_scm_example')" - -SETUP_PY_FILES = { - "setup.py": SETUP_PY_WITH_NAME, - "setup.cfg": SETUP_PY_PLAIN, - "pyproject tool.setuptools_scm": SETUP_PY_PLAIN, - "pyproject.project": SETUP_PY_PLAIN, -} - -SETUP_CFG_FILES = { - "setup.py": "", - "setup.cfg": "[metadata]\nname=setuptools_scm_example", - "pyproject tool.setuptools_scm": "", - "pyproject.project": "", -} - -with_metadata_in = pytest.mark.parametrize( - "metadata_in", - ["setup.py", "setup.cfg", "pyproject tool.setuptools_scm", "pyproject.project"], -) - - -@with_metadata_in -def test_pyproject_support_with_git(wd: WorkDir, metadata_in: str) -> None: - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Write files first - if metadata_in == "pyproject tool.setuptools_scm": - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=80", "setuptools-scm>=8"] - build-backend = "setuptools.build_meta" - - [tool.setuptools_scm] - dist_name='setuptools_scm_example' - """ - ), - ) - elif metadata_in == "pyproject.project": - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=80", "setuptools-scm>=8"] - build-backend = "setuptools.build_meta" - - [project] - name='setuptools_scm_example' - dynamic=['version'] - [tool.setuptools_scm] - """ - ), - ) - else: - # For "setup.py" and "setup.cfg" cases, use the PYPROJECT_FILES content - wd.write("pyproject.toml", PYPROJECT_FILES[metadata_in]) - - wd.write("setup.py", SETUP_PY_FILES[metadata_in]) - wd.write("setup.cfg", SETUP_CFG_FILES[metadata_in]) - - # Now do git operations - wd("git init") - wd("git config user.email test@example.com") - wd('git config user.name "a test"') - wd("git add .") - wd('git commit -m "initial"') - wd("git tag v1.0.0") - - res = run([sys.executable, "setup.py", "--version"], wd.cwd) - assert res.stdout == "1.0.0" - - -def test_pyproject_no_project_section_no_auto_activation(wd: WorkDir) -> None: - """Test that setuptools_scm doesn't auto-activate when pyproject.toml has no project section.""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Create pyproject.toml with setuptools-scm in build-system.requires but no project section - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=80", "setuptools-scm>=8"] - build-backend = "setuptools.build_meta" - """ - ), - ) - - wd.write("setup.py", "__import__('setuptools').setup(name='test_package')") - - # Now do git operations - wd("git init") - wd("git config user.email test@example.com") - wd('git config user.name "a test"') - wd("git add .") - wd('git commit -m "initial"') - wd("git tag v1.0.0") - - # Should not auto-activate setuptools_scm, so version should be None - res = run([sys.executable, "setup.py", "--version"], wd.cwd) - print(f"Version output: {res.stdout!r}") - # The version should not be from setuptools_scm (which would be 1.0.0 from git tag) - # but should be the default setuptools version (0.0.0) - assert res.stdout == "0.0.0" # Default version when no version is set - - -def test_pyproject_no_project_section_no_error(wd: WorkDir) -> None: - """Test that setuptools_scm doesn't raise an error when there's no project section.""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Create pyproject.toml with setuptools-scm in build-system.requires but no project section - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=80", "setuptools-scm>=8"] - build-backend = "setuptools.build_meta" - """ - ), - ) - - # This should NOT raise an error because there's no project section - # setuptools_scm should simply not auto-activate - from setuptools_scm._integration.pyproject_reading import read_pyproject - - pyproject_data = read_pyproject(wd.cwd / "pyproject.toml") - # Should not auto-activate when no project section exists - assert not pyproject_data.is_required or not pyproject_data.section_present - - -@pytest.mark.parametrize("use_scm_version", ["True", "{}", "lambda: {}"]) -def test_pyproject_missing_setup_hook_works(wd: WorkDir, use_scm_version: str) -> None: - wd.write( - "setup.py", - f"""__import__('setuptools').setup( - name="example-scm-unique", - use_scm_version={use_scm_version}, - )""", - ) - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires=["setuptools", "setuptools_scm"] - build-backend = "setuptools.build_meta" - [tool.setuptools_scm] - """ - ), - ) - - res = subprocess.run( - [sys.executable, "setup.py", "--version"], - cwd=wd.cwd, - check=True, - stdout=subprocess.PIPE, - encoding="utf-8", - ) - stripped = res.stdout.strip() - assert stripped.endswith("0.1.dev0+d20090213") - - res_build = subprocess.run( - [sys.executable, "-m", "build", "-nxw"], - env={k: v for k, v in os.environ.items() if k != "SETUPTOOLS_SCM_DEBUG"}, - cwd=wd.cwd, - ) - import pprint - - pprint.pprint(res_build) - wheel: Path = next(wd.cwd.joinpath("dist").iterdir()) - assert "0.1.dev0+d20090213" in str(wheel) - - def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY, "1.0.0") @@ -271,18 +76,6 @@ def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: assert wd.get_version(dist_name="ignored") == "1.0.0" -@with_metadata_in -def test_pretend_version_named_pyproject_integration( - monkeypatch: pytest.MonkeyPatch, wd: WorkDir, metadata_in: str -) -> None: - test_pyproject_support_with_git(wd, metadata_in) - monkeypatch.setenv( - PRETEND_KEY_NAMED.format(name="setuptools_scm_example".upper()), "3.2.1" - ) - res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("3.2.1") - - def test_pretend_version_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test2".upper()), "2.0.0") @@ -303,7 +96,6 @@ def test_pretend_version_rejects_invalid_string( ) -> None: """Test that invalid pretend versions raise errors and bubble up.""" monkeypatch.setenv(PRETEND_KEY, "dummy") - wd.write("setup.py", SETUP_PY_PLAIN) # With strict validation, invalid pretend versions should raise errors with pytest.raises(Exception, match=r".*dummy.*"): @@ -323,7 +115,6 @@ def test_pretend_metadata_with_version( assert version == "1.2.3.dev4+g1337beef" # Test version file template functionality - wd.write("setup.py", SETUP_PY_PLAIN) wd("mkdir -p src") version_file_content = """ version = '{version}' @@ -400,7 +191,6 @@ def test_pretend_metadata_with_scm_version( assert "1.0.1.dev7+gcustom123" == version # Test version file to see if metadata was applied - wd.write("setup.py", SETUP_PY_PLAIN) wd("mkdir -p src") version_file_content = """ version = '{version}' @@ -628,7 +418,7 @@ def test_distribution_provides_extras() -> None: dist = distribution("setuptools_scm") pe: list[str] = dist.metadata.get_all("Provides-Extra", []) - assert sorted(pe) == ["rich", "toml"] + assert sorted(pe) == ["rich", "simple", "toml"] @pytest.mark.issue(760) @@ -644,11 +434,66 @@ def test_unicode_in_setup_cfg(tmp_path: Path) -> None: ), encoding="utf-8", ) - from setuptools_scm._integration.setup_cfg import read_dist_name_from_setup_cfg + from setuptools_scm._integration.setup_cfg import read_setup_cfg - name = read_dist_name_from_setup_cfg(cfg) + name = read_setup_cfg(cfg).name assert name == "configparser" + # also ensure we can parse a version if present (legacy projects) + cfg.write_text( + textwrap.dedent( + """ + [metadata] + name = configparser + version = 1.2.3 + """ + ), + encoding="utf-8", + ) + + data = read_setup_cfg(cfg) + assert isinstance(data, SetuptoolsBasicData) + assert data.name == "configparser" + assert data.version == "1.2.3" + + +def test_setup_cfg_version_prevents_inference_version_keyword( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + # Legacy project setup - we construct the data directly since files are not read anyway + monkeypatch.chdir(tmp_path) + + dist = create_clean_distribution("legacy-proj") + + # Using keyword should detect an existing version via legacy data and avoid inferring + from setuptools_scm._integration import setuptools as setuptools_integration + from setuptools_scm._integration.pyproject_reading import PyProjectData + from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData + + # Construct PyProjectData directly without requiring build backend inference + pyproject_data = PyProjectData.for_testing( + is_required=False, # setuptools-scm not required + section_present=False, # no [tool.setuptools_scm] section + project_present=False, # no [project] section + ) + + # Construct legacy data with version from setup.cfg + legacy_data = SetuptoolsBasicData( + path=tmp_path / "setup.cfg", name="legacy-proj", version="0.9.0" + ) + + with pytest.warns(UserWarning, match="version of legacy-proj already set"): + setuptools_integration.version_keyword( + dist, + "use_scm_version", + True, + _given_pyproject_data=pyproject_data, + _given_legacy_data=legacy_data, + ) + + # setuptools_scm should not set a version when setup.cfg already provided one + assert dist.metadata.version is None + def test_setuptools_version_keyword_ensures_regex( wd: WorkDir, @@ -683,143 +528,6 @@ def test_git_archival_plugin_ignored(tmp_path: Path, ep_name: str) -> None: assert "setuptools_scm_git_archive:parse" not in imports -def test_pyproject_build_system_requires_setuptools_scm(wd: WorkDir) -> None: - """Test that setuptools_scm is enabled when present in build-system.requires""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Test with setuptools_scm in build-system.requires but no [tool.setuptools_scm] section - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=64", "setuptools_scm>=8"] - build-backend = "setuptools.build_meta" - - [project] - name = "test-package" - dynamic = ["version"] - """ - ), - ) - wd.write("setup.py", "__import__('setuptools').setup()") - - res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("0.1.dev0+d20090213") - - -def test_pyproject_build_system_requires_setuptools_scm_dash_variant( - wd: WorkDir, -) -> None: - """Test that setuptools-scm (dash variant) is also detected in build-system.requires""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Test with setuptools-scm (dash variant) in build-system.requires - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=64", "setuptools-scm>=8"] - build-backend = "setuptools.build_meta" - - [project] - name = "test-package" - dynamic = ["version"] - """ - ), - ) - wd.write("setup.py", "__import__('setuptools').setup()") - - res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("0.1.dev0+d20090213") - - -def test_pyproject_build_system_requires_with_extras(wd: WorkDir) -> None: - """Test that setuptools_scm[toml] is detected in build-system.requires""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Test with setuptools_scm[toml] (with extras) in build-system.requires - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=64", "setuptools_scm[toml]>=8"] - build-backend = "setuptools.build_meta" - - [project] - name = "test-package" - dynamic = ["version"] - """ - ), - ) - wd.write("setup.py", "__import__('setuptools').setup()") - - res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("0.1.dev0+d20090213") - - -def test_pyproject_build_system_requires_not_present(wd: WorkDir) -> None: - """Test that version is not set when setuptools_scm is not in build-system.requires and no [tool.setuptools_scm] section""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Test without setuptools_scm in build-system.requires and no [tool.setuptools_scm] section - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=64", "wheel"] - build-backend = "setuptools.build_meta" - - [project] - name = "test-package" - dynamic = ["version"] - """ - ), - ) - wd.write("setup.py", "__import__('setuptools').setup()") - - res = wd([sys.executable, "setup.py", "--version"]) - assert res == "0.0.0" - - -def test_pyproject_build_system_requires_priority_over_tool_section( - wd: WorkDir, -) -> None: - """Test that both build-system.requires and [tool.setuptools_scm] section work together""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Test with both setuptools_scm in build-system.requires AND [tool.setuptools_scm] section - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=64", "setuptools_scm>=8"] - build-backend = "setuptools.build_meta" - - [project] - name = "test-package" - dynamic = ["version"] - - [tool.setuptools_scm] - # empty section, should work with build-system detection - """ - ), - ) - wd.write("setup.py", "__import__('setuptools').setup()") - - res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("0.1.dev0+d20090213") - - @pytest.mark.parametrize("base_name", ["setuptools_scm", "setuptools-scm"]) @pytest.mark.parametrize( "requirements", @@ -831,93 +539,6 @@ def test_extract_package_name(base_name: str, requirements: str) -> None: assert extract_package_name(f"{base_name}{requirements}") == "setuptools-scm" -def test_build_requires_integration_with_config_reading(wd: WorkDir) -> None: - """Test that Configuration.from_file handles build-system.requires automatically""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - from setuptools_scm._config import Configuration - - # Test: pyproject.toml with setuptools_scm in build-system.requires but no tool section - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=64", "setuptools_scm>=8"] - - [project] - name = "test-package" - dynamic = ["version"] - """ - ), - ) - - # This should NOT raise an error because setuptools_scm is in build-system.requires - config = Configuration.from_file( - name=wd.cwd.joinpath("pyproject.toml"), dist_name="test-package" - ) - assert config.dist_name == "test-package" - - # Test: pyproject.toml with setuptools-scm (dash variant) in build-system.requires - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires = ["setuptools>=64", "setuptools-scm>=8"] - - [project] - name = "test-package" - dynamic = ["version"] - """ - ), - ) - - # This should also NOT raise an error - config = Configuration.from_file( - name=wd.cwd.joinpath("pyproject.toml"), dist_name="test-package" - ) - assert config.dist_name == "test-package" - - -def test_improved_error_message_mentions_both_config_options( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that the error message mentions both configuration options""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Create pyproject.toml without setuptools_scm configuration - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [project] - name = "test-package" - - [build-system] - requires = ["setuptools>=64"] - """ - ), - ) - - from setuptools_scm._config import Configuration - - with pytest.raises(LookupError) as exc_info: - Configuration.from_file( - name=wd.cwd.joinpath("pyproject.toml"), - dist_name="test-package", - missing_file_ok=False, - ) - - error_msg = str(exc_info.value) - # Check that the error message mentions both configuration options - assert "tool.setuptools_scm" in error_msg - assert "build-system" in error_msg - assert "requires" in error_msg - - # Helper function for creating and managing distribution objects def create_clean_distribution(name: str) -> setuptools.Distribution: """Create a clean distribution object without any setuptools_scm effects. @@ -939,33 +560,35 @@ def create_clean_distribution(name: str) -> setuptools.Distribution: return dist -def version_keyword_default(dist: setuptools.Distribution) -> None: +def version_keyword_default( + dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None +) -> None: """Helper to call version_keyword with default config and return the result.""" - setuptools_integration.version_keyword(dist, "use_scm_version", True) + setuptools_integration.version_keyword( + dist, "use_scm_version", True, _given_pyproject_data=pyproject_data + ) -def version_keyword_calver(dist: setuptools.Distribution) -> None: +def version_keyword_calver( + dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None +) -> None: """Helper to call version_keyword with calver-by-date scheme and return the result.""" setuptools_integration.version_keyword( - dist, "use_scm_version", {"version_scheme": "calver-by-date"} + dist, + "use_scm_version", + {"version_scheme": "calver-by-date"}, + _given_pyproject_data=pyproject_data, ) -# Test cases: (first_func, second_func, expected_final_version) -# We use a controlled date to make calver deterministic -TEST_CASES = [ - # Real-world scenarios: infer_version and version_keyword can be called in either order - (setuptools_integration.infer_version, version_keyword_default, "1.0.1.dev1"), - ( - setuptools_integration.infer_version, - version_keyword_calver, - "9.2.13.0.dev1", - ), # calver should win but doesn't - (version_keyword_default, setuptools_integration.infer_version, "1.0.1.dev1"), - (version_keyword_calver, setuptools_integration.infer_version, "9.2.13.0.dev1"), -] +def infer_version_with_data( + dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None +) -> None: + """Helper to call infer_version with pyproject data.""" + + setuptools_integration.infer_version(dist, _given_pyproject_data=pyproject_data) @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/1022") @@ -975,7 +598,13 @@ def version_keyword_calver(dist: setuptools.Distribution) -> None: ) @pytest.mark.parametrize( ("first_integration", "second_integration", "expected_final_version"), - TEST_CASES, + [ + # infer_version and version_keyword can be called in either order + (infer_version_with_data, version_keyword_default, "1.0.1.dev1"), + (infer_version_with_data, version_keyword_calver, "9.2.13.0.dev1"), + (version_keyword_default, infer_version_with_data, "1.0.1.dev1"), + (version_keyword_calver, infer_version_with_data, "9.2.13.0.dev1"), + ], ) def test_integration_function_call_order( wd: WorkDir, @@ -992,7 +621,9 @@ def test_integration_function_call_order( # Set up controlled environment for deterministic versions monkeypatch.setenv("SOURCE_DATE_EPOCH", "1234567890") # 2009-02-13T23:31:30+00:00 # Override node_date to get consistent calver versions - monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_METADATA", "{node_date=2009-02-13}") + monkeypatch.setenv( + "SETUPTOOLS_SCM_PRETEND_METADATA_FOR_TEST_CALL_ORDER", "{node_date=2009-02-13}" + ) # Set up a git repository with a tag and known commit hash wd.commit_testfile("test") @@ -1000,28 +631,21 @@ def test_integration_function_call_order( wd.commit_testfile("test2") # Add another commit to get distance monkeypatch.chdir(wd.cwd) - # Create a pyproject.toml file - pyproject_content = f""" -[build-system] -requires = ["setuptools", "setuptools_scm"] -build-backend = "setuptools.build_meta" - -[project] -name = "test-pkg-{first_integration.__name__}-{second_integration.__name__}" -dynamic = ["version"] - -[tool.setuptools_scm] -local_scheme = "no-local-version" -""" - wd.write("pyproject.toml", pyproject_content) - - dist = create_clean_distribution( - f"test-pkg-{first_integration.__name__}-{second_integration.__name__}" + # Create PyProjectData with equivalent configuration - no file I/O! + project_name = "test-call-order" + pyproject_data = PyProjectData.for_testing( + project_name=project_name, + has_dynamic_version=True, + project_present=True, + section_present=True, + local_scheme="no-local-version", ) - # Call both integration functions in order - first_integration(dist) - second_integration(dist) + dist = create_clean_distribution(project_name) + + # Call both integration functions in order with direct data injection + first_integration(dist, pyproject_data) + second_integration(dist, pyproject_data) # Get the final version directly from the distribution final_version = dist.metadata.version @@ -1033,280 +657,6 @@ def test_integration_function_call_order( ) -def test_infer_version_with_build_requires_no_tool_section( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that infer_version works when setuptools-scm is in build_requires but no [tool.setuptools_scm] section""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Set up a git repository with a tag - wd.commit_testfile("test") - wd("git tag 1.0.0") - monkeypatch.chdir(wd.cwd) - - # Create a pyproject.toml file with setuptools_scm in build-system.requires but NO [tool.setuptools_scm] section - pyproject_content = """ -[build-system] -requires = ["setuptools>=80", "setuptools_scm>=8"] -build-backend = "setuptools.build_meta" - -[project] -name = "test-package-infer-version" -dynamic = ["version"] -""" - wd.write("pyproject.toml", pyproject_content) - - from setuptools_scm._integration.setuptools import infer_version - - # Create clean distribution - dist = create_clean_distribution("test-package-infer-version") - - # Call infer_version - this should work because setuptools_scm is in build-system.requires - infer_version(dist) - - # Verify that version was set - assert dist.metadata.version is not None - assert dist.metadata.version == "1.0.0" - - # Verify that the marker was set - assert getattr(dist, "_setuptools_scm_version_set_by_infer", False) is True - - -def test_infer_version_with_build_requires_dash_variant_no_tool_section( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that infer_version works when setuptools-scm (dash variant) is in build_requires but no [tool.setuptools_scm] section""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Set up a git repository with a tag - wd.commit_testfile("test") - wd("git tag 1.0.0") - monkeypatch.chdir(wd.cwd) - - # Create a pyproject.toml file with setuptools-scm (dash variant) in build-system.requires but NO [tool.setuptools_scm] section - pyproject_content = """ -[build-system] -requires = ["setuptools>=80", "setuptools-scm>=8"] -build-backend = "setuptools.build_meta" - -[project] -name = "test-package-infer-version-dash" -dynamic = ["version"] -""" - wd.write("pyproject.toml", pyproject_content) - - from setuptools_scm._integration.setuptools import infer_version - - # Create clean distribution - dist = create_clean_distribution("test-package-infer-version-dash") - - # Call infer_version - this should work because setuptools-scm is in build-system.requires - infer_version(dist) - - # Verify that version was set - assert dist.metadata.version is not None - assert dist.metadata.version == "1.0.0" - - # Verify that the marker was set - assert getattr(dist, "_setuptools_scm_version_set_by_infer", False) is True - - -def test_infer_version_without_build_requires_no_tool_section_silently_returns( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that infer_version silently returns when setuptools-scm is NOT in build_requires and no [tool.setuptools_scm] section""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Set up a git repository with a tag - wd.commit_testfile("test") - wd("git tag 1.0.0") - monkeypatch.chdir(wd.cwd) - - # Create a pyproject.toml file WITHOUT setuptools_scm in build-system.requires and NO [tool.setuptools_scm] section - pyproject_content = """ -[build-system] -requires = ["setuptools>=80", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "test-package-no-scm" -dynamic = ["version"] -""" - wd.write("pyproject.toml", pyproject_content) - - from setuptools_scm._integration.setuptools import infer_version - - # Create clean distribution - dist = create_clean_distribution("test-package-no-scm") - - infer_version(dist) - assert dist.metadata.version is None - - -def test_version_keyword_no_scm_dependency_works( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - # Set up a git repository with a tag - wd.commit_testfile("test") - wd("git tag 1.0.0") - monkeypatch.chdir(wd.cwd) - - # Create a pyproject.toml file WITHOUT setuptools_scm in build-system.requires - # and WITHOUT [tool.setuptools_scm] section - pyproject_content = """ -[build-system] -requires = ["setuptools>=80"] -build-backend = "setuptools.build_meta" - -[project] -name = "test-package-no-scm" -dynamic = ["version"] -""" - wd.write("pyproject.toml", pyproject_content) - - import setuptools - - from setuptools_scm._integration.setuptools import version_keyword - - # Create distribution - dist = setuptools.Distribution({"name": "test-package-no-scm"}) - - version_keyword(dist, "use_scm_version", True) - assert dist.metadata.version == "1.0.0" - - -def test_verify_dynamic_version_when_required_missing_dynamic( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that verification fails when setuptools-scm is in build-system.requires but dynamic=['version'] is missing""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Change to the test directory - monkeypatch.chdir(wd.cwd) - - # Create a pyproject.toml file with setuptools-scm in build-system.requires but NO dynamic=['version'] - pyproject_content = """ -[build-system] -requires = ["setuptools>=80", "setuptools-scm>=8"] -build-backend = "setuptools.build_meta" - -[project] -name = "test-package-missing-dynamic" -# Missing: dynamic = ["version"] -""" - wd.write("pyproject.toml", pyproject_content) - - from setuptools_scm._integration.pyproject_reading import read_pyproject - - # This should raise a ValueError because dynamic=['version'] is missing - with pytest.raises( - ValueError, match="dynamic=\\['version'\\] is not set in \\[project\\]" - ): - read_pyproject(Path("pyproject.toml"), missing_section_ok=True) - - -def test_verify_dynamic_version_when_required_with_tool_section( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that verification passes when setuptools-scm is in build-system.requires and [tool.setuptools_scm] section exists""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Change to the test directory - monkeypatch.chdir(wd.cwd) - - # Create a pyproject.toml file with setuptools-scm in build-system.requires and [tool.setuptools_scm] section - pyproject_content = """ -[build-system] -requires = ["setuptools>=80", "setuptools-scm>=8"] -build-backend = "setuptools.build_meta" - -[project] -name = "test-package-with-tool-section" -# Missing: dynamic = ["version"] - -[tool.setuptools_scm] -""" - wd.write("pyproject.toml", pyproject_content) - - from setuptools_scm._integration.pyproject_reading import read_pyproject - - # This should not raise an error because [tool.setuptools_scm] section exists - pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True) - assert pyproject_data.is_required is True - assert pyproject_data.section_present is True - - -def test_verify_dynamic_version_when_required_with_dynamic( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that verification passes when setuptools-scm is in build-system.requires and dynamic=['version'] is set""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Change to the test directory - monkeypatch.chdir(wd.cwd) - - # Create a pyproject.toml file with setuptools-scm in build-system.requires and dynamic=['version'] - pyproject_content = """ -[build-system] -requires = ["setuptools>=80", "setuptools-scm>=8"] -build-backend = "setuptools.build_meta" - -[project] -name = "test-package-with-dynamic" -dynamic = ["version"] -""" - wd.write("pyproject.toml", pyproject_content) - - from setuptools_scm._integration.pyproject_reading import read_pyproject - - # This should not raise an error because dynamic=['version'] is set - pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True) - assert pyproject_data.is_required is True - assert pyproject_data.section_present is False - - -def test_infer_version_logs_debug_when_missing_dynamic_version( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that infer_version logs debug info when setuptools-scm is in build-system.requires but dynamic=['version'] is missing""" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - - # Set up a git repository with a tag - wd.commit_testfile("test") - wd("git tag 1.0.0") - monkeypatch.chdir(wd.cwd) - - # Create a pyproject.toml file with setuptools-scm in build-system.requires but NO dynamic=['version'] - pyproject_content = """ -[build-system] -requires = ["setuptools>=80", "setuptools-scm>=8"] -build-backend = "setuptools.build_meta" - -[project] -name = "test-package-missing-dynamic" -# Missing: dynamic = ["version"] -""" - wd.write("pyproject.toml", pyproject_content) - - from setuptools_scm._integration.setuptools import infer_version - - # Create clean distribution - dist = create_clean_distribution("test-package-missing-dynamic") - - # This should not raise an error, but should log debug info about the configuration issue - infer_version(dist) - - # Verify that version was not set due to configuration issue - assert dist.metadata.version is None - - @pytest.mark.issue("xmlsec-regression") def test_xmlsec_download_regression( tmp_path: Path, monkeypatch: pytest.MonkeyPatch @@ -1331,7 +681,6 @@ def test_xmlsec_download_regression( "xmlsec==1.3.16", ], cwd=tmp_path, - text=True, timeout=300, check=True, ) diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py index 592adf86..1962882a 100644 --- a/testing/test_pyproject_reading.py +++ b/testing/test_pyproject_reading.py @@ -4,31 +4,17 @@ import pytest +from setuptools_scm._integration.pyproject_reading import has_build_package_with_extra from setuptools_scm._integration.pyproject_reading import read_pyproject class TestPyProjectReading: """Test the pyproject reading functionality.""" - def test_read_pyproject_missing_file_ok(self, tmp_path: Path) -> None: - """Test that read_pyproject handles missing files when missing_file_ok=True.""" - # Test with missing_file_ok=True - result = read_pyproject( - path=tmp_path / "nonexistent.toml", missing_file_ok=True - ) - - assert result.path == tmp_path / "nonexistent.toml" - assert result.tool_name == "setuptools_scm" - assert result.project == {} - assert result.section == {} - assert result.is_required is False - assert result.section_present is False - assert result.project_present is False - - def test_read_pyproject_missing_file_not_ok(self, tmp_path: Path) -> None: - """Test that read_pyproject raises FileNotFoundError when missing_file_ok=False.""" + def test_read_pyproject_missing_file_raises(self, tmp_path: Path) -> None: + """Test that read_pyproject raises FileNotFoundError when file is missing.""" with pytest.raises(FileNotFoundError): - read_pyproject(path=tmp_path / "nonexistent.toml", missing_file_ok=False) + read_pyproject(path=tmp_path / "nonexistent.toml") def test_read_pyproject_existing_file(self, tmp_path: Path) -> None: """Test that read_pyproject reads existing files correctly.""" @@ -55,3 +41,70 @@ def test_read_pyproject_existing_file(self, tmp_path: Path) -> None: assert result.section_present is True assert result.project_present is True assert result.project.get("name") == "test-package" + + +class TestBuildPackageWithExtra: + """Test the has_build_package_with_extra function.""" + + def test_has_simple_extra(self) -> None: + """Test that simple extra is detected correctly.""" + requires = ["setuptools-scm[simple]"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is True + ) + + def test_has_no_simple_extra(self) -> None: + """Test that missing simple extra is detected correctly.""" + requires = ["setuptools-scm"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) + + def test_has_different_extra(self) -> None: + """Test that different extra is not detected as simple.""" + requires = ["setuptools-scm[toml]"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) + + def test_has_multiple_extras_including_simple(self) -> None: + """Test that simple extra is detected when multiple extras are present.""" + requires = ["setuptools-scm[simple,toml]"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is True + ) + + def test_different_package_with_simple_extra(self) -> None: + """Test that simple extra on different package is not detected.""" + requires = ["other-package[simple]"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) + + def test_version_specifier_with_extra(self) -> None: + """Test that version specifiers work correctly with extras.""" + requires = ["setuptools-scm[simple]>=8.0"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is True + ) + + def test_complex_requirement_with_extra(self) -> None: + """Test that complex requirements with extras work correctly.""" + requires = ["setuptools-scm[simple]>=8.0,<9.0"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is True + ) + + def test_empty_requires_list(self) -> None: + """Test that empty requires list returns False.""" + requires: list[str] = [] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) + + def test_invalid_requirement_string(self) -> None: + """Test that invalid requirement strings are handled gracefully.""" + requires = ["invalid requirement string"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index 5bdd2861..967ab768 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -1,278 +1,255 @@ from __future__ import annotations +from types import SimpleNamespace +from typing import Any + +import pytest + from setuptools_scm._integration.pyproject_reading import PyProjectData from setuptools_scm._integration.version_inference import VersionInferenceConfig -from setuptools_scm._integration.version_inference import VersionInferenceError -from setuptools_scm._integration.version_inference import VersionInferenceException from setuptools_scm._integration.version_inference import VersionInferenceNoOp +from setuptools_scm._integration.version_inference import VersionInferenceResult +from setuptools_scm._integration.version_inference import VersionInferenceWarning from setuptools_scm._integration.version_inference import get_version_inference_config - -class TestVersionInferenceDecision: - """Test the version inference decision logic.""" - - def test_version_already_set_by_infer_with_overrides(self) -> None: - """Test that we proceed when version was set by infer_version but overrides provided.""" - result = get_version_inference_config( - dist_name="test_package", - current_version="1.0.0", - pyproject_data=PyProjectData.for_testing(True, True, True), - overrides={"key": "value"}, - was_set_by_infer=True, - ) - - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - assert result.overrides == {"key": "value"} - - def test_version_already_set_by_infer_no_overrides(self) -> None: - """Test that we allow re-inferring when version was set by infer_version and overrides=None (another infer_version call).""" - result = get_version_inference_config( - dist_name="test_package", - current_version="1.0.0", - pyproject_data=PyProjectData.for_testing(True, True, True), - overrides=None, - was_set_by_infer=True, - ) - - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - assert result.overrides is None - - def test_version_already_set_by_infer_empty_overrides(self) -> None: - """Test that we don't re-infer when version was set by infer_version with empty overrides (version_keyword call).""" - result = get_version_inference_config( - dist_name="test_package", - current_version="1.0.0", - pyproject_data=PyProjectData.for_testing(True, True, True), - overrides={}, - was_set_by_infer=True, - ) - - assert isinstance(result, VersionInferenceNoOp) - - def test_version_already_set_by_something_else(self) -> None: - """Test that we return error when version was set by something else.""" - result = get_version_inference_config( - dist_name="test_package", - current_version="1.0.0", - pyproject_data=PyProjectData.for_testing(True, True, True), - overrides=None, - was_set_by_infer=False, - ) - - assert isinstance(result, VersionInferenceError) - assert result.message == "version of test_package already set" - assert result.should_warn is True - - def test_setuptools_scm_package(self) -> None: - """Test that we don't infer for setuptools-scm package itself.""" - result = get_version_inference_config( - dist_name="setuptools-scm", - current_version=None, - pyproject_data=PyProjectData.for_testing(True, True, True), - ) - - assert isinstance(result, VersionInferenceNoOp) - - def test_no_pyproject_toml(self) -> None: - """Test that we don't infer when no pyproject.toml exists.""" - # When no pyproject.toml exists, the integration points should return early - # and not call get_version_inference_config at all. - # This test is no longer needed as pyproject_data is always required. - - def test_no_setuptools_scm_config_infer_version(self) -> None: - """Test that we don't infer when setuptools-scm is not configured and infer_version called.""" - result = get_version_inference_config( - dist_name="test_package", - current_version=None, - pyproject_data=PyProjectData.for_testing(False, False, True), - overrides=None, # infer_version call +# Common test data +PYPROJECT = SimpleNamespace( + DEFAULT=PyProjectData.for_testing( + is_required=True, section_present=True, project_present=True + ), + WITHOUT_TOOL_SECTION=PyProjectData.for_testing( + is_required=True, section_present=False, project_present=True + ), + ONLY_REQUIRED=PyProjectData.for_testing( + is_required=True, section_present=False, project_present=False + ), + WITHOUT_PROJECT=PyProjectData.for_testing( + is_required=True, section_present=True, project_present=False + ), +) + +OVERRIDES = SimpleNamespace( + NOT_GIVEN=None, + EMPTY={}, + CALVER={"version_scheme": "calver"}, + UNRELATED={"key": "value"}, +) + + +WARNING_PACKAGE = VersionInferenceWarning( + message="version of test_package already set", +) +WARNING_NO_PACKAGE = VersionInferenceWarning( + message="version of None already set", +) + +NOOP = VersionInferenceNoOp() + + +def expect_config( + *, + dist_name: str | None = "test_package", + current_version: str | None, + pyproject_data: PyProjectData = PYPROJECT.DEFAULT, + overrides: dict[str, Any] | None = None, + expected: type[VersionInferenceConfig] + | VersionInferenceWarning + | VersionInferenceNoOp, +) -> None: + """Helper to test get_version_inference_config and assert expected result type.""" + __tracebackhide__ = True + result = get_version_inference_config( + dist_name=dist_name, + current_version=current_version, + pyproject_data=pyproject_data, + overrides=overrides, + ) + + expectation: VersionInferenceResult + if expected == VersionInferenceConfig: + expectation = VersionInferenceConfig( + dist_name=dist_name, + pyproject_data=pyproject_data, + overrides=overrides, ) + else: + assert isinstance(expected, (VersionInferenceNoOp, VersionInferenceWarning)) + expectation = expected + + assert result == expectation + + +infer_implied = pytest.mark.parametrize( + ("overrides", "pyproject_data"), + [ + pytest.param( + OVERRIDES.EMPTY, PYPROJECT.DEFAULT, id="empty_overrides_default_pyproject" + ), + pytest.param( + OVERRIDES.EMPTY, + PYPROJECT.WITHOUT_TOOL_SECTION, + id="empty_overrides_without_tool_section", + ), + pytest.param( + OVERRIDES.NOT_GIVEN, + PYPROJECT.DEFAULT, + id="infer_version_default_pyproject", + ), + ], +) + + +@pytest.mark.parametrize("package_name", ["test_package", None]) +@infer_implied +def test_implied_with_version_warns( + package_name: str | None, + overrides: dict[str, Any] | None, + pyproject_data: PyProjectData, +) -> None: + expect_config( + dist_name=package_name, + current_version="1.0.0", + pyproject_data=pyproject_data, + overrides=overrides, + expected=WARNING_PACKAGE if package_name else WARNING_NO_PACKAGE, + ) + + +@pytest.mark.parametrize("package_name", ["test_package", None]) +@infer_implied +def test_implied_without_version_infers( + package_name: str | None, + overrides: dict[str, Any] | None, + pyproject_data: PyProjectData, +) -> None: + expect_config( + dist_name=package_name, + current_version=None, + pyproject_data=pyproject_data, + overrides=overrides, + expected=VersionInferenceConfig, + ) + + +def test_no_config_no_infer() -> None: + expect_config( + current_version=None, + pyproject_data=PYPROJECT.WITHOUT_TOOL_SECTION, + overrides=OVERRIDES.NOT_GIVEN, + expected=NOOP, + ) - assert isinstance(result, VersionInferenceNoOp) - - def test_no_setuptools_scm_config_version_keyword(self) -> None: - """Test that we DO infer when setuptools-scm is not configured but use_scm_version=True.""" - result = get_version_inference_config( - dist_name="test_package", - current_version=None, - pyproject_data=PyProjectData.for_testing(False, False, True), - overrides={}, # version_keyword call with use_scm_version=True - ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - assert result.overrides == {} +class TestVersionInferenceDecision: + """Test the version inference decision logic.""" def test_setuptools_scm_required_no_project_section_infer_version(self) -> None: - """Test that we don't infer when setuptools-scm is required but no project section and infer_version called.""" - result = get_version_inference_config( - dist_name="test_package", + """We don't infer without tool section even if required: infer_version path.""" + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, False, False), - overrides=None, # infer_version call + pyproject_data=PYPROJECT.ONLY_REQUIRED, + overrides=None, + expected=NOOP, ) - assert isinstance(result, VersionInferenceNoOp) - def test_setuptools_scm_required_no_project_section_version_keyword(self) -> None: """Test that we DO infer when setuptools-scm is required but no project section and use_scm_version=True.""" - result = get_version_inference_config( - dist_name="test_package", + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, False, False), - overrides={}, # version_keyword call with use_scm_version=True + pyproject_data=PYPROJECT.ONLY_REQUIRED, + overrides=OVERRIDES.EMPTY, + expected=VersionInferenceConfig, ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - assert result.overrides == {} - def test_setuptools_scm_required_no_project_section_version_keyword_with_config( self, ) -> None: """Test that we DO infer when setuptools-scm is required but no project section and use_scm_version={config}.""" - overrides = {"version_scheme": "calver"} - result = get_version_inference_config( - dist_name="test_package", + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, False, False), - overrides=overrides, # version_keyword call with use_scm_version={config} + pyproject_data=PYPROJECT.ONLY_REQUIRED, + overrides=OVERRIDES.CALVER, + expected=VersionInferenceConfig, ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - assert result.overrides == overrides - - def test_setuptools_scm_required_with_project_section(self) -> None: - """Test that we infer when setuptools-scm is required and project section exists.""" - result = get_version_inference_config( - dist_name="test_package", + def test_tool_section_present(self) -> None: + """We infer when tool section is present.""" + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, False, True), + pyproject_data=PYPROJECT.WITHOUT_PROJECT, + expected=VersionInferenceConfig, ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - - def test_tool_section_present(self) -> None: - """Test that we infer when tool section is present.""" - result = get_version_inference_config( - dist_name="test_package", + def test_simple_extra_with_dynamic_version_infers(self) -> None: + """We infer when setuptools-scm[simple] is in build-system.requires and version is dynamic.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + has_dynamic_version=True, + build_requires=["setuptools-scm[simple]"], + ) + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(False, True, False), + pyproject_data=pyproject_data, + expected=VersionInferenceConfig, ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - - def test_both_required_and_tool_section(self) -> None: - """Test that we infer when both required and tool section are present.""" - result = get_version_inference_config( - dist_name="test_package", + def test_simple_extra_without_dynamic_version_no_infer(self) -> None: + """We don't infer when setuptools-scm[simple] is present but version is not dynamic.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + has_dynamic_version=False, + build_requires=["setuptools-scm[simple]"], + ) + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, True, True), + pyproject_data=pyproject_data, + expected=NOOP, ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - - def test_none_dist_name(self) -> None: - """Test that we handle None dist_name correctly.""" - result = get_version_inference_config( - dist_name=None, + def test_no_simple_extra_with_dynamic_version_no_infer(self) -> None: + """We don't infer when setuptools-scm (without simple extra) is present even with dynamic version.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + has_dynamic_version=True, + build_requires=["setuptools-scm"], + ) + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, True, True), + pyproject_data=pyproject_data, + expected=NOOP, ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name is None - - def test_version_already_set_none_dist_name(self) -> None: - """Test that we handle None dist_name in error case.""" - result = get_version_inference_config( - dist_name=None, - current_version="1.0.0", - pyproject_data=PyProjectData.for_testing(True, True, True), - overrides=None, - was_set_by_infer=False, + def test_simple_extra_no_project_section_no_infer(self) -> None: + """We don't infer when setuptools-scm[simple] is present but no project section.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=False, + build_requires=["setuptools-scm[simple]"], ) - - assert isinstance(result, VersionInferenceError) - assert result.message == "version of None already set" - - def test_overrides_passed_through(self) -> None: - """Test that overrides are passed through to the config.""" - overrides = {"version_scheme": "calver"} - result = get_version_inference_config( - dist_name="test_package", + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, True, True), - overrides=overrides, + pyproject_data=pyproject_data, + expected=NOOP, ) - assert isinstance(result, VersionInferenceConfig) - assert result.overrides == overrides - - -class TestPyProjectData: - """Test the PyProjectData dataclass.""" - - def test_pyproject_data_creation(self) -> None: - """Test creating PyProjectData instances.""" - data = PyProjectData.for_testing(True, False, True) - assert data.is_required is True - assert data.section_present is False - assert data.project_present is True - - def test_pyproject_data_equality(self) -> None: - """Test PyProjectData equality.""" - data1 = PyProjectData.for_testing(True, False, True) - data2 = PyProjectData.for_testing(True, False, True) - data3 = PyProjectData.for_testing(False, False, True) - - assert data1 == data2 - assert data1 != data3 - - -class TestVersionInferenceConfig: - """Test the VersionInferenceConfig dataclass.""" - - def test_config_creation(self) -> None: - """Test creating VersionInferenceConfig instances.""" - pyproject_data = PyProjectData.for_testing(True, True, True) - config = VersionInferenceConfig( - dist_name="test_package", + def test_simple_extra_with_version_warns(self) -> None: + """We warn when setuptools-scm[simple] is present with dynamic version but version is already set.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + has_dynamic_version=True, + build_requires=["setuptools-scm[simple]"], + ) + expect_config( + current_version="1.0.0", pyproject_data=pyproject_data, - overrides={"key": "value"}, + expected=WARNING_PACKAGE, ) - - assert config.dist_name == "test_package" - assert config.pyproject_data == pyproject_data - assert config.overrides == {"key": "value"} - - -class TestVersionInferenceError: - """Test the VersionInferenceError dataclass.""" - - def test_error_creation(self) -> None: - """Test creating VersionInferenceError instances.""" - error = VersionInferenceError("test message", should_warn=True) - assert error.message == "test message" - assert error.should_warn is True - - def test_error_default_warn(self) -> None: - """Test VersionInferenceError default should_warn value.""" - error = VersionInferenceError("test message") - assert error.should_warn is False - - -class TestVersionInferenceException: - """Test the VersionInferenceException dataclass.""" - - def test_exception_creation(self) -> None: - """Test creating VersionInferenceException instances.""" - original_exception = ValueError("test error") - wrapper = VersionInferenceException(original_exception) - assert wrapper.exception == original_exception diff --git a/uv.lock b/uv.lock index e28d70b0..4b145a91 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.8" resolution-markers = [ "python_full_version >= '3.11'", @@ -1048,6 +1048,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, ] +[[package]] +name = "mypy" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532, upload-time = "2024-10-22T21:55:47.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731, upload-time = "2024-10-22T21:54:54.221Z" }, + { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276, upload-time = "2024-10-22T21:54:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706, upload-time = "2024-10-22T21:55:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586, upload-time = "2024-10-22T21:55:18.957Z" }, + { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318, upload-time = "2024-10-22T21:55:13.791Z" }, + { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027, upload-time = "2024-10-22T21:55:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699, upload-time = "2024-10-22T21:55:34.646Z" }, + { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263, upload-time = "2024-10-22T21:54:51.807Z" }, + { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688, upload-time = "2024-10-22T21:55:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811, upload-time = "2024-10-22T21:54:59.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900, upload-time = "2024-10-22T21:55:37.103Z" }, + { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818, upload-time = "2024-10-22T21:55:11.513Z" }, + { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275, upload-time = "2024-10-22T21:54:37.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783, upload-time = "2024-10-22T21:55:42.852Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197, upload-time = "2024-10-22T21:54:43.68Z" }, + { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721, upload-time = "2024-10-22T21:54:22.321Z" }, + { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996, upload-time = "2024-10-22T21:54:46.023Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043, upload-time = "2024-10-22T21:55:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996, upload-time = "2024-10-22T21:55:25.811Z" }, + { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709, upload-time = "2024-10-22T21:55:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147, upload-time = "2024-10-22T21:55:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373, upload-time = "2024-10-22T21:54:56.889Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621, upload-time = "2024-10-22T21:54:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348, upload-time = "2024-10-22T21:54:40.801Z" }, + { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311, upload-time = "2024-10-22T21:54:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906, upload-time = "2024-10-22T21:55:28.105Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657, upload-time = "2024-10-22T21:55:03.931Z" }, + { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394, upload-time = "2024-10-22T21:54:49.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591, upload-time = "2024-10-22T21:55:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690, upload-time = "2024-10-22T21:54:28.814Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043, upload-time = "2024-10-22T21:55:16.617Z" }, +] + +[[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 = "packaging" version = "25.0" @@ -1707,6 +1761,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/f6/5fc0574af5379606ffd57a4b68ed88f9b415eb222047fe023aefcc00a648/rich_argparse-1.7.1-py3-none-any.whl", hash = "sha256:a8650b42e4a4ff72127837632fba6b7da40784842f08d7395eb67a9cbd7b4bf9", size = 25357, upload-time = "2025-05-25T20:20:33.793Z" }, ] +[[package]] +name = "ruff" +version = "0.12.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, + { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, + { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, + { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, + { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, +] + [[package]] name = "setuptools" version = "75.3.2" @@ -1770,12 +1849,14 @@ test = [ { name = "flake8", version = "7.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "griffe", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mypy" }, { name = "pip", version = "25.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pip", version = "25.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest-timeout" }, { name = "rich" }, + { name = "ruff" }, { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, { name = "wheel" }, @@ -1789,7 +1870,7 @@ requires-dist = [ { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -provides-extras = ["rich", "toml"] +provides-extras = ["rich", "simple", "toml"] [package.metadata.requires-dev] docs = [ @@ -1804,10 +1885,12 @@ test = [ { name = "build" }, { name = "flake8" }, { name = "griffe" }, + { name = "mypy", specifier = "~=1.13.0" }, { name = "pip" }, { name = "pytest" }, { name = "pytest-timeout" }, { name = "rich" }, + { name = "ruff" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "wheel" }, ]