From e4cd419a988e86c4ec075fab6908f47a8b21becc Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 27 Jul 2025 23:37:13 +0200 Subject: [PATCH 1/4] fix #1150: enable if setuptools_scm is part of the projects own build-requires --- CHANGELOG.md | 1 + src/setuptools_scm/_config.py | 27 +- .../_integration/pyproject_reading.py | 37 ++- src/setuptools_scm/_integration/setuptools.py | 22 +- testing/test_integration.py | 236 ++++++++++++++++++ 5 files changed, 311 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 565245b4..5e521149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - improve typing for entry_points - refactor file modification time logic into shared helper function for better maintainability - reduce complexity of HgWorkdir.get_meta method by extracting focused helper methods +- fix #1150: enable setuptools-scm when we are a build requirement ### Fixed diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 6ed520f9..45bcf8e2 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -14,6 +14,7 @@ from . import _log from . import _types as _t +from ._integration.pyproject_reading import PyProjectData from ._integration.pyproject_reading import ( get_args_for_pyproject as _get_args_for_pyproject, ) @@ -115,17 +116,31 @@ def from_file( cls, name: str | os.PathLike[str] = "pyproject.toml", dist_name: str | None = None, - _require_section: bool = True, + missing_file_ok: bool = False, **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 the [tool.setuptools_scm] section. + 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). """ - pyproject_data = _read_pyproject(Path(name), require_section=_require_section) + try: + pyproject_data = _read_pyproject(Path(name)) + except FileNotFoundError: + if missing_file_ok: + log.warning("File %s not found, using empty configuration", name) + pyproject_data = PyProjectData( + path=Path(name), + tool_name="setuptools_scm", + project={}, + section={}, + is_required=False, + ) + else: + raise 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 0e4f9aa1..baf850d1 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import NamedTuple +from typing import Sequence from .. import _log from .setuptools import read_dist_name_from_setup_cfg @@ -20,30 +21,56 @@ class PyProjectData(NamedTuple): tool_name: str project: TOML_RESULT section: TOML_RESULT + is_required: bool @property def project_name(self) -> str | None: return self.project.get("name") +def has_build_package( + requires: Sequence[str], build_package_names: Sequence[str] +) -> bool: + for requirement in requires: + import re + + # Remove extras like [toml] first + clean_req = re.sub(r"\[.*?\]", "", requirement) + # Split on version operators and take first part + package_name = re.split(r"[><=!~]", clean_req)[0].strip().lower() + if package_name in build_package_names: + return True + return False + + def read_pyproject( path: Path = Path("pyproject.toml"), tool_name: str = "setuptools_scm", - require_section: bool = True, + build_package_names: Sequence[str] = ("setuptools_scm", "setuptools-scm"), ) -> PyProjectData: - defn = read_toml_content(path, None if require_section else {}) + defn = read_toml_content(path) + requires: list[str] = defn.get("build-system", {}).get("requires", []) + is_required = has_build_package(requires, build_package_names) + try: section = defn.get("tool", {})[tool_name] except LookupError as e: - error = f"{path} does not contain a tool.{tool_name} section" - if require_section: + if not is_required: + # 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 = {} project = defn.get("project", {}) - return PyProjectData(path, tool_name, project, section) + return PyProjectData(path, tool_name, project, section, is_required) def get_args_for_pyproject( diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 55ca1660..7a9a577a 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -45,6 +45,25 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: ) +def _extract_package_name(requirement: str) -> str: + """Extract the package name from a requirement string. + + Examples: + 'setuptools_scm' -> 'setuptools_scm' + 'setuptools-scm>=8' -> 'setuptools-scm' + 'setuptools_scm[toml]>=7.0' -> 'setuptools_scm' + """ + # Split on common requirement operators and take the first part + # This handles: >=, <=, ==, !=, >, <, ~= + import re + + # Remove extras like [toml] first + requirement = re.sub(r"\[.*?\]", "", requirement) + # Split on version operators + package_name = re.split(r"[><=!~]", requirement)[0].strip() + return package_name + + def _assign_version( dist: setuptools.Distribution, config: _config.Configuration ) -> None: @@ -97,7 +116,7 @@ def version_keyword( config = _config.Configuration.from_file( dist_name=dist_name, - _require_section=False, + missing_file_ok=True, **overrides, ) _assign_version(dist, config) @@ -115,6 +134,7 @@ def infer_version(dist: setuptools.Distribution) -> None: return if dist_name == "setuptools-scm": return + try: config = _config.Configuration.from_file(dist_name=dist_name) except LookupError as e: diff --git a/testing/test_integration.py b/testing/test_integration.py index ba1cdb67..b73f0807 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -13,6 +13,7 @@ import setuptools_scm._integration.setuptools from setuptools_scm import Configuration +from setuptools_scm._integration.setuptools import _extract_package_name from setuptools_scm._integration.setuptools import _warn_on_old_setuptools from setuptools_scm._overrides import PRETEND_KEY from setuptools_scm._overrides import PRETEND_KEY_NAMED @@ -256,3 +257,238 @@ def test_git_archival_plugin_ignored(tmp_path: Path, ep_name: str) -> None: found = list(iter_matching_entrypoints(tmp_path, config=c, entrypoint=ep_name)) imports = [item.value for item in found] 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") + + +def test_extract_package_name() -> None: + """Test the _extract_package_name helper function""" + assert _extract_package_name("setuptools_scm") == "setuptools_scm" + assert _extract_package_name("setuptools-scm") == "setuptools-scm" + assert _extract_package_name("setuptools_scm>=8") == "setuptools_scm" + assert _extract_package_name("setuptools-scm>=8") == "setuptools-scm" + assert _extract_package_name("setuptools_scm[toml]>=7.0") == "setuptools_scm" + assert _extract_package_name("setuptools-scm[toml]>=7.0") == "setuptools-scm" + assert _extract_package_name("setuptools_scm==8.0.0") == "setuptools_scm" + assert _extract_package_name("setuptools_scm~=8.0") == "setuptools_scm" + assert _extract_package_name("setuptools_scm[rich,toml]>=8") == "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" + """ + ), + ) + + # 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" + """ + ), + ) + + # 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 From 8b2e7513dcb1bc6e3d958439e0c1920d4b7c875a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 27 Jul 2025 23:40:56 +0200 Subject: [PATCH 2/4] add docs for the now optional tool section --- README.md | 18 ++++++++++++++++++ docs/config.md | 12 ++++++++++++ docs/usage.md | 43 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7722ba44..7d9c056a 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,24 @@ dynamic = ["version"] [tool.setuptools_scm] ``` +!!! note "Simplified Configuration" + + Starting with setuptools-scm 8.1+, if `setuptools_scm` (or `setuptools-scm`) is + present in your `build-system.requires`, the `[tool.setuptools_scm]` section + becomes optional! You can now enable setuptools-scm with just: + + ```toml title="pyproject.toml" + [build-system] + requires = ["setuptools>=64", "setuptools-scm>=8"] + build-backend = "setuptools.build_meta" + + [project] + dynamic = ["version"] + ``` + + The `[tool.setuptools_scm]` section is only needed if you want to customize + configuration options. + Additionally, a version file can be written by specifying: ```toml title="pyproject.toml" diff --git a/docs/config.md b/docs/config.md index 8f6210bb..0f80ca94 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,17 @@ # Configuration +## When is configuration needed? + +Starting with setuptools-scm 8.1+, explicit configuration is **optional** in many cases: + +- **No configuration needed**: If `setuptools_scm` (or `setuptools-scm`) is in your `build-system.requires`, setuptools-scm will automatically activate with sensible defaults. + +- **Configuration recommended**: 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`) + - Configure fallback behavior (`fallback_version`) + - Or any other non-default behavior ## configuration parameters diff --git a/docs/usage.md b/docs/usage.md index ed3b0f8d..faede38c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,10 +2,30 @@ ## At build time -The preferred way to configure `setuptools-scm` is to author -settings in the `tool.setuptools_scm` section of `pyproject.toml`. +There are two ways to configure `setuptools-scm` at build time, depending on your needs: -It's necessary to use a setuptools version released after 2022. +### Automatic Configuration (Recommended for Simple Cases) + +For projects that don't need custom configuration, simply include `setuptools-scm` +in your build requirements: + +```toml title="pyproject.toml" +[build-system] +requires = ["setuptools>=64", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +# version = "0.0.1" # Remove any existing version parameter. +dynamic = ["version"] +``` + +**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. + +### Explicit Configuration + +If you need to customize setuptools-scm behavior, use the `tool.setuptools_scm` section: ```toml title="pyproject.toml" [build-system] @@ -17,14 +37,25 @@ build-backend = "setuptools.build_meta" dynamic = ["version"] [tool.setuptools_scm] -# can be empty if no extra settings are needed, presence enables setuptools-scm +# Configure custom options here (version schemes, file writing, etc.) +version_file = "src/mypackage/_version.py" ``` -That will be sufficient to require `setuptools-scm` for projects -that support PEP 518 ([pip](https://pypi.org/project/pip) and +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 +!!! info "How Automatic Detection Works" + + When setuptools-scm is listed in `build-system.requires`, it automatically detects this during the build process and activates with default settings. This means: + + - ✅ **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 + + Both package names are detected: `setuptools_scm` and `setuptools-scm` (with dash). + ### Version files Version files can be created with the ``version_file`` directive. From 1b4cc28963c807c50745de64f4fef8cf95c1bf6d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 27 Jul 2025 23:43:08 +0200 Subject: [PATCH 3/4] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e521149..93faafdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - refactor file modification time logic into shared helper function for better maintainability - reduce complexity of HgWorkdir.get_meta method by extracting focused helper methods - fix #1150: enable setuptools-scm when we are a build requirement +- feature #1154: add the commit id the the default version file template + ### Fixed From 1a4dbb58b4f283a477fd45fc07944b351bb71c36 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 28 Jul 2025 09:12:05 +0200 Subject: [PATCH 4/4] fix #1059: add scm version overrides via env vars --- CHANGELOG.md | 2 +- docs/overrides.md | 61 ++++++++ src/setuptools_scm/_get_version_impl.py | 8 +- src/setuptools_scm/_overrides.py | 121 ++++++++++++++++ testing/test_integration.py | 182 ++++++++++++++++++++++++ uv.lock | 57 -------- 6 files changed, 372 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93faafdb..08d838f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - add `setuptools-scm` console_scripts entry point to make the CLI directly executable - make Mercurial command configurable by environment variable `SETUPTOOLS_SCM_HG_COMMAND` - fix #1099 use file modification times for dirty working directory timestamps instead of current time - +- fix #1059: add `SETUPTOOLS_SCM_PRETEND_METADATA` environment variable to override individual ScmVersion fields ### Changed - add `pip` to test optional dependencies for improved uv venv compatibility diff --git a/docs/overrides.md b/docs/overrides.md index 5a6093bb..942bde30 100644 --- a/docs/overrides.md +++ b/docs/overrides.md @@ -10,6 +10,67 @@ as the override source for the version number unparsed string. to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}` where the dist name normalization follows adapted PEP 503 semantics. +## pretend metadata + +setuptools-scm provides a mechanism to override individual version metadata fields at build time. + +The environment variable `SETUPTOOLS_SCM_PRETEND_METADATA` accepts a TOML inline table +with field overrides for the ScmVersion object. + +To be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${NORMALIZED_DIST_NAME}` +where the dist name normalization follows adapted PEP 503 semantics. + +### Supported fields + +The following ScmVersion fields can be overridden: + +- `distance` (int): Number of commits since the tag +- `node` (str): The commit hash/node identifier +- `dirty` (bool): Whether the working directory has uncommitted changes +- `branch` (str): The branch name +- `node_date` (date): The date of the commit (TOML date format: `2024-01-15`) +- `time` (datetime): The version timestamp (TOML datetime format) +- `preformatted` (bool): Whether the version string is preformatted +- `tag`: The version tag (can be string or version object) + +### Examples + +Override commit hash and distance: +```bash +export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}' +``` + +Override multiple fields with proper TOML types: +```bash +export SETUPTOOLS_SCM_PRETEND_METADATA='{node="gabcdef12", distance=7, dirty=true, node_date=2024-01-15}' +``` + +Use with a specific package: +```bash +export SETUPTOOLS_SCM_PRETEND_METADATA_FOR_MY_PACKAGE='{node="g1234567", distance=2}' +``` + +### Use case: CI/CD environments + +This is particularly useful for solving issues where version file templates need access to +commit metadata that may not be available in certain build environments: + +```toml +[tool.setuptools_scm] +version_file = "src/mypackage/_version.py" +version_file_template = ''' +version = "{version}" +commit_hash = "{scm_version.node}" +commit_count = {scm_version.distance} +''' +``` + +With pretend metadata, you can ensure the template gets the correct values: +```bash +export SETUPTOOLS_SCM_PRETEND_VERSION="1.2.3.dev4+g1337beef" +export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}' +``` + ## config overrides setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` diff --git a/src/setuptools_scm/_get_version_impl.py b/src/setuptools_scm/_get_version_impl.py index cced45e2..40217cc6 100644 --- a/src/setuptools_scm/_get_version_impl.py +++ b/src/setuptools_scm/_get_version_impl.py @@ -56,12 +56,18 @@ def parse_fallback_version(config: Configuration) -> ScmVersion | None: def parse_version(config: Configuration) -> ScmVersion | None: - return ( + # First try to get a version from the normal flow + scm_version = ( _read_pretended_version_for(config) or parse_scm_version(config) or parse_fallback_version(config) ) + # Apply any metadata overrides to the version we found + from ._overrides import _apply_metadata_overrides + + return _apply_metadata_overrides(scm_version, config) + def write_version_files( config: Configuration, version: str, scm_version: ScmVersion diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py index ee9269a7..698f0fa3 100644 --- a/src/setuptools_scm/_overrides.py +++ b/src/setuptools_scm/_overrides.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import os import re @@ -14,6 +15,8 @@ PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION" PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}" +PRETEND_METADATA_KEY = "SETUPTOOLS_SCM_PRETEND_METADATA" +PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}" def read_named_env( @@ -30,6 +33,124 @@ def read_named_env( return os.environ.get(f"{tool}_{name}") +def _read_pretended_metadata_for( + config: _config.Configuration, +) -> dict[str, Any] | None: + """read overridden metadata from the environment + + tries ``SETUPTOOLS_SCM_PRETEND_METADATA`` + and ``SETUPTOOLS_SCM_PRETEND_METADATA_FOR_$UPPERCASE_DIST_NAME`` + + Returns a dictionary with metadata field overrides like: + {"node": "g1337beef", "distance": 4} + """ + log.debug("dist name: %s", config.dist_name) + + pretended = read_named_env(name="PRETEND_METADATA", dist_name=config.dist_name) + + if pretended: + try: + metadata_overrides = load_toml_or_inline_map(pretended) + # Validate that only known ScmVersion fields are provided + valid_fields = { + "tag", + "distance", + "node", + "dirty", + "preformatted", + "branch", + "node_date", + "time", + } + invalid_fields = set(metadata_overrides.keys()) - valid_fields + if invalid_fields: + log.warning( + "Invalid metadata fields in pretend metadata: %s. " + "Valid fields are: %s", + invalid_fields, + valid_fields, + ) + # Remove invalid fields but continue processing + for field in invalid_fields: + metadata_overrides.pop(field) + + return metadata_overrides or None + except Exception as e: + log.error("Failed to parse pretend metadata: %s", e) + return None + else: + return None + + +def _apply_metadata_overrides( + scm_version: version.ScmVersion | None, + config: _config.Configuration, +) -> version.ScmVersion | None: + """Apply metadata overrides to a ScmVersion object. + + This function reads pretend metadata from environment variables and applies + the overrides to the given ScmVersion. TOML type coercion is used so values + should be provided in their correct types (int, bool, datetime, etc.). + + Args: + scm_version: The ScmVersion to apply overrides to, or None + config: Configuration object + + Returns: + Modified ScmVersion with overrides applied, or None + """ + metadata_overrides = _read_pretended_metadata_for(config) + + if not metadata_overrides: + return scm_version + + if scm_version is None: + log.warning( + "PRETEND_METADATA specified but no base version found. " + "Metadata overrides cannot be applied without a base version." + ) + return None + + log.info("Applying metadata overrides: %s", metadata_overrides) + + # Define type checks and field mappings + from datetime import date + from datetime import datetime + + field_specs: dict[str, tuple[type | tuple[type, type], str]] = { + "distance": (int, "int"), + "dirty": (bool, "bool"), + "preformatted": (bool, "bool"), + "node_date": (date, "date"), + "time": (datetime, "datetime"), + "node": ((str, type(None)), "str or None"), + "branch": ((str, type(None)), "str or None"), + # tag is special - can be multiple types, handled separately + } + + # Apply each override individually using dataclasses.replace for type safety + result = scm_version + + for field, value in metadata_overrides.items(): + if field in field_specs: + expected_type, type_name = field_specs[field] + assert isinstance(value, expected_type), ( + f"{field} must be {type_name}, got {type(value).__name__}: {value!r}" + ) + result = dataclasses.replace(result, **{field: value}) + elif field == "tag": + # tag can be Version, NonNormalizedVersion, or str - we'll let the assignment handle validation + result = dataclasses.replace(result, tag=value) + else: + # This shouldn't happen due to validation in _read_pretended_metadata_for + log.warning("Unknown field '%s' in metadata overrides", field) + + # Ensure config is preserved (should not be overridden) + assert result.config is config, "Config must be preserved during metadata overrides" + + return result + + def _read_pretended_version_for( config: _config.Configuration, ) -> version.ScmVersion | None: diff --git a/testing/test_integration.py b/testing/test_integration.py index b73f0807..8b853b2d 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib.metadata +import logging import os import subprocess import sys @@ -194,6 +195,187 @@ def test_pretend_version_accepts_bad_string( assert pyver == "0.0.0" +def test_pretend_metadata_with_version( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir +) -> None: + """Test pretend metadata overrides work with pretend version.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "1.2.3.dev4+g1337beef") + monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1337beef", distance=4}') + + version = wd.get_version() + assert version == "1.2.3.dev4+g1337beef" + + # Test version file template functionality + wd.write("setup.py", SETUP_PY_PLAIN) + wd("mkdir -p src") # Create the src directory + # This is a template string, not an f-string - used by setuptools-scm templating + version_file_content = """ +version = '{version}' +major = {version_tuple[0]} +minor = {version_tuple[1]} +patch = {version_tuple[2]} +commit_hash = '{scm_version.node}' +num_commit = {scm_version.distance} +""" # noqa: RUF027 + # Use write_to with template to create version file + version = wd.get_version( + write_to="src/version.py", write_to_template=version_file_content + ) + + # Read the file using pathlib + content = (wd.cwd / "src/version.py").read_text() + assert "commit_hash = 'g1337beef'" in content + assert "num_commit = 4" in content + + +def test_pretend_metadata_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: + """Test pretend metadata with named package support.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY_NAMED + + monkeypatch.setenv( + PRETEND_KEY_NAMED.format(name="test".upper()), "1.2.3.dev5+gabcdef12" + ) + monkeypatch.setenv( + PRETEND_METADATA_KEY_NAMED.format(name="test".upper()), + '{node="gabcdef12", distance=5, dirty=true}', + ) + + version = wd.get_version(dist_name="test") + assert version == "1.2.3.dev5+gabcdef12" + + +def test_pretend_metadata_without_version_warns( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture +) -> None: + """Test that pretend metadata without any base version logs a warning.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + # Only set metadata, no version - but there will be a git repo so there will be a base version + # Let's create an empty git repo without commits to truly have no base version + monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1234567", distance=2}') + + # Should get a version with fallback but metadata overrides applied + with caplog.at_level(logging.WARNING): + version = wd.get_version() + # Should get a fallback version with metadata overrides + assert version is not None + + # In this case, metadata was applied to a fallback version, so no warning about missing base + + +def test_pretend_metadata_with_scm_version( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that pretend metadata works with actual SCM-detected version.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + # Set up a git repo with a tag so we have a base version + wd("git init") + wd("git config user.name test") + wd("git config user.email test@example.com") + wd.write("file.txt", "content") + wd("git add file.txt") + wd("git commit -m 'initial'") + wd("git tag v1.0.0") + + # Now add metadata overrides + monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="gcustom123", distance=7}') + + # Test that the metadata gets applied to the actual SCM version + version = wd.get_version() + # The version becomes 1.0.1.dev7+gcustom123 due to version scheme and metadata overrides + 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") + # This is a template string, not an f-string - used by setuptools-scm templating + version_file_content = """ +version = '{version}' +commit_hash = '{scm_version.node}' +num_commit = {scm_version.distance} +""" # noqa: RUF027 + version = wd.get_version( + write_to="src/version.py", write_to_template=version_file_content + ) + + content = (wd.cwd / "src/version.py").read_text() + assert "commit_hash = 'gcustom123'" in content + assert "num_commit = 7" in content + + +def test_pretend_metadata_type_conversion( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir +) -> None: + """Test that pretend metadata properly uses TOML native types.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "2.0.0") + monkeypatch.setenv( + PRETEND_METADATA_KEY, + '{distance=10, dirty=true, node="gfedcba98", branch="feature-branch"}', + ) + + version = wd.get_version() + # The version should be formatted properly with the metadata + assert "2.0.0" in version + + +def test_pretend_metadata_invalid_fields_filtered( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture +) -> None: + """Test that invalid metadata fields are filtered out with a warning.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "1.0.0") + monkeypatch.setenv( + PRETEND_METADATA_KEY, + '{node="g123456", distance=3, invalid_field="should_be_ignored", another_bad_field=42}', + ) + + with caplog.at_level(logging.WARNING): + version = wd.get_version() + assert version == "1.0.0" + + assert "Invalid metadata fields in pretend metadata" in caplog.text + assert "invalid_field" in caplog.text + assert "another_bad_field" in caplog.text + + +def test_pretend_metadata_date_parsing( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir +) -> None: + """Test that TOML date values work in pretend metadata.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "1.5.0") + monkeypatch.setenv( + PRETEND_METADATA_KEY, '{node="g987654", distance=7, node_date=2024-01-15}' + ) + + version = wd.get_version() + assert version == "1.5.0" + + +def test_pretend_metadata_invalid_toml_error( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture +) -> None: + """Test that invalid TOML in pretend metadata logs an error.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "1.0.0") + monkeypatch.setenv(PRETEND_METADATA_KEY, "{invalid toml syntax here}") + + with caplog.at_level(logging.ERROR): + version = wd.get_version() + # Should fall back to basic pretend version + assert version == "1.0.0" + + assert "Failed to parse pretend metadata" in caplog.text + + def testwarn_on_broken_setuptools() -> None: _warn_on_old_setuptools("61") with pytest.warns(RuntimeWarning, match="ERROR: setuptools==60"): diff --git a/uv.lock b/uv.lock index e51dd5d8..b5aa1c46 100644 --- a/uv.lock +++ b/uv.lock @@ -685,61 +685,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mercurial" -version = "7.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/ac/d526af69836382fc3b084bf7221475f18440c26eba68f8efee76fb92db50/mercurial-7.0.3.tar.gz", hash = "sha256:59fc84640524da6f1938ea7e4eb0cd579fc7fedaaf563a916cb4f9dac0eacf6c", size = 8984723, upload-time = "2025-07-15T18:40:47.533Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/12/9c4a7cfd1bc6001da1c19971c83e135a9c2bdc6ebe786ad2cd3d1cb8d9f3/mercurial-7.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3909a00f3137f28111039c279dc3e0056322c43dff572574059cede4ec3ad47c", size = 5229018, upload-time = "2025-07-15T18:42:00.263Z" }, - { url = "https://files.pythonhosted.org/packages/18/00/4148b5023e504f3506723918543b2ced0502c3b331f4e92640b359614fa3/mercurial-7.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49f147eef7c3a8d32fc8db8c4971b6c5fdb5686b236266d926b959c86d27775c", size = 7419969, upload-time = "2025-07-15T18:39:57.581Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1b/9ace9ba78958cbad8caba69ff2267a2a456ffca8171904115bf149d06bc4/mercurial-7.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23d73d15d2aebf4b12096acd4105795d99cdc2feb53aa1b5ac148311ee9c38b6", size = 7116443, upload-time = "2025-07-15T18:40:00.452Z" }, - { url = "https://files.pythonhosted.org/packages/d0/75/4e18ed74ae32bd97fdccf9d840017385b807c1552748d4056c5111522f68/mercurial-7.0.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:07f19bb5361af29d44f39bde07a81c48c7fa5182543c25a3027cda172287358a", size = 7082677, upload-time = "2025-07-15T18:40:02.615Z" }, - { url = "https://files.pythonhosted.org/packages/11/76/8b60eb51eeec122b7aee494d6ceaab04a82ec57392f8c2703cc27c7ad2c3/mercurial-7.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ebbcea4c948d5c341a8820020bf8752396f820b3f52be2af13ab7bf96f4449c5", size = 7308226, upload-time = "2025-07-15T18:40:04.692Z" }, - { url = "https://files.pythonhosted.org/packages/b1/66/ded86508dedf6450bb0d9ded6dbc1a2c2bc3723261151d679c09c0ca1ad7/mercurial-7.0.3-cp310-cp310-win32.whl", hash = "sha256:a7c7a57c2376d67e99f421b9d0cb3715b87cb6dc032dc80d6522545c25879cde", size = 6378578, upload-time = "2025-07-15T18:42:12.026Z" }, - { url = "https://files.pythonhosted.org/packages/f6/5f/a47bef8ea4943df87d00528e9d7c526884594a746e0dd97eab584228d7b3/mercurial-7.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:352403f5f96da137ceb6f1deb9335f692e87109502ae20aa0057cad9ea7221f1", size = 6639819, upload-time = "2025-07-15T18:42:14.367Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a1/8d35a163bf1ad0d63d6e2a2bf6fcb2281106d04ac515b3c04c4abbf7693b/mercurial-7.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:470275c6448dd10cc24d31b4408da34ed03ccf2a1acd745475a7544b4ba59e0e", size = 6604641, upload-time = "2025-07-15T18:42:16.463Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/e1070f8e2a752b72c3bcc33c67c25be9d9f4d98b8b33768602de397833cd/mercurial-7.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5a57309c06080f2165158fa0e089c79549e92a649da1415c00eccb531c7355cd", size = 5229089, upload-time = "2025-07-15T18:42:02.062Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ba/7c3ac692ce29a572d48afdb1b5723856c36d9bf912d9418806f0dadf5fdf/mercurial-7.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8add51af1e7f1efa1e040fd1c9d8f0c414e13414285ef728e7e825ec62e74ae3", size = 7432369, upload-time = "2025-07-15T18:40:06.409Z" }, - { url = "https://files.pythonhosted.org/packages/76/e1/acf6a368011a99078e67facb365d5b9cc84d72d346248b570c2f7b3249ee/mercurial-7.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c122e44003631211b038a582566b3615979b998ee3273abbe662ff1b2af95d2", size = 7128747, upload-time = "2025-07-15T18:40:08.425Z" }, - { url = "https://files.pythonhosted.org/packages/3b/8a/d4279cab89e3928d04ccd2e5ba616bf97c9a6fcef1b43389b58a20e88ff0/mercurial-7.0.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4ea6982b1652f0dffe0c22ff6cfc263f47fba2065362ce13cc4a6b39f9e4a350", size = 7093495, upload-time = "2025-07-15T18:40:10.848Z" }, - { url = "https://files.pythonhosted.org/packages/db/dc/eb2e10eba2532eccde2db825d386bd9bb9a3f9d157d9295e9fe6c0df7e8d/mercurial-7.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:839cc9f5819539490ccf310265cdd2d90439d07de9bed637d7f60e5606c077f3", size = 7319262, upload-time = "2025-07-15T18:40:12.563Z" }, - { url = "https://files.pythonhosted.org/packages/e2/31/aa96115ee6d31277c4300d4f3838203428ccc115b3179bb17b2652061cad/mercurial-7.0.3-cp311-cp311-win32.whl", hash = "sha256:c3028c23ead2dea0f600a5198679f752c0c76a4a68dfa0a375642f13c29e0f2c", size = 6667609, upload-time = "2025-07-15T18:42:18.664Z" }, - { url = "https://files.pythonhosted.org/packages/a0/39/f46899a6317443166159bea338703e23fad075c6a83718a8c92da830cf87/mercurial-7.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:5cbd147d0385d576b1c7a976a0708feda122086463ed44b16d973ff9c263e92e", size = 6966311, upload-time = "2025-07-15T18:42:20.841Z" }, - { url = "https://files.pythonhosted.org/packages/db/97/640966ffb3b0900d8cdb1d17c07e5b29173a7cb2be301d51374f65140bfb/mercurial-7.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:0a6f630786a4b02d102348bd69ec51bee204ea9af27ca8a38f885c93dac7050a", size = 6931243, upload-time = "2025-07-15T18:42:22.533Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3e/49a963ab7e1ec394a6e63d5918c4f129894d581f9dd54e0194671c4b0be5/mercurial-7.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cd33e0f66a33687e9fb78f7c5ad7358d1a0ba9893b88be6a0990aa494e710fb3", size = 5229731, upload-time = "2025-07-15T18:42:04.315Z" }, - { url = "https://files.pythonhosted.org/packages/2b/00/7059b1e65465ba5cbb7abc9d69bc109892166b23061c92d263d59a6fa795/mercurial-7.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b38063caabeda4d79277a0b5b87861ee6d960ba0335797a213804f72240193a2", size = 7433795, upload-time = "2025-07-15T18:40:17.146Z" }, - { url = "https://files.pythonhosted.org/packages/10/75/59dc05bdd2ae5873715d840f5a6b8342f4cdd43d8d164c656d1d24f96f62/mercurial-7.0.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15b76fe0a1bd1a9ea7ca95c77540c5961ecad2e1928a06d28064f47556f7d8f4", size = 7126592, upload-time = "2025-07-15T18:40:19.517Z" }, - { url = "https://files.pythonhosted.org/packages/8a/c0/c79c2ef062cc7c02eb7e69d82ed3eb80cabd6d84da259c802e3f3cb2413f/mercurial-7.0.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:14488a534ed1e9d277c52ed4f9e5332c3f688856280cafd496cfe5cf13425aed", size = 7095911, upload-time = "2025-07-15T18:40:21.198Z" }, - { url = "https://files.pythonhosted.org/packages/04/00/f2a768a92fd20a455f77054abd6b55ab937e7f79c9f9829915b4d8fd54c5/mercurial-7.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:89802ae2ffaed6762a536c42762a1b2acebdf9e0a9432dde19ffc1f90c178b94", size = 7320409, upload-time = "2025-07-15T18:40:23.329Z" }, - { url = "https://files.pythonhosted.org/packages/ea/15/842e07bc84a040246b572e7d1bffc9d8c3a3fa4a805e69bb9d5fc3e8f2da/mercurial-7.0.3-cp312-cp312-win32.whl", hash = "sha256:4cfcea7a3f7dcf83bf4c1dc13c5353264cc62203f4ab39815882f35d028b54d3", size = 6893343, upload-time = "2025-07-15T18:42:24.718Z" }, - { url = "https://files.pythonhosted.org/packages/91/5d/038ab8795ba2825c61b72f2b4d5d278a6fd805ea4b55b7c748e99f9a18a0/mercurial-7.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:6b87856811a5042cafc28848864d71bcf9f8c848f5c85525b752421aca427090", size = 7217450, upload-time = "2025-07-15T18:42:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/63/08/0792467b540d6544c062c1be1154925c6bd6ca7c688665f175184ba541e1/mercurial-7.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:8aee550a4d330934cc72dc43eccde8695de1bd389559710b21871104b585532d", size = 7182042, upload-time = "2025-07-15T18:42:29.355Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d3/af97c2ef0ae568efdf3492fb6ce9660201d0f218537b8c701305fd8aa0bb/mercurial-7.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3f30d3b512476fed2a0c73b32f7336980c7e36ff1bd2239afea9ee61c9f18c4", size = 5229550, upload-time = "2025-07-15T18:42:06.035Z" }, - { url = "https://files.pythonhosted.org/packages/ff/2b/46e423902f111d1c8b7a2cdc27635fc274fb1cd9361c6e5f95c8b4215456/mercurial-7.0.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57ba0cd8329aa393397526cfa9c92557a46813d7fa3f3eb3d5616d3e49b43678", size = 7434216, upload-time = "2025-07-15T18:40:25.102Z" }, - { url = "https://files.pythonhosted.org/packages/66/48/c6288e88d00405e7cddfc9eca07fde12c902cd7f6c3e247063e90bc8c189/mercurial-7.0.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c751a7a424198ffbee3695ee98e5e60d6c50cab884e42ae16cdb229cb9d90f74", size = 7126513, upload-time = "2025-07-15T18:40:26.811Z" }, - { url = "https://files.pythonhosted.org/packages/64/95/3396e5bd885f0a64d2d280bf144399aac1d6dbfeef6c17f065487160ee61/mercurial-7.0.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:14e1c2292372f0ca9e95db38637da47d2114a34d910d6b4f276c5f0aa37e1f74", size = 7096315, upload-time = "2025-07-15T18:40:28.811Z" }, - { url = "https://files.pythonhosted.org/packages/4c/af/675c03b7e21407c74ed6e249c435559ea291871416f1ceb614c00381d8a1/mercurial-7.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1ab6d3e16d5741a52eaf9bd5d86f5daeb5e9c5d474cfb6796233c7bbb5206c7", size = 7320758, upload-time = "2025-07-15T18:40:30.532Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ac/2dc91adf495451cbc03b49377d9dd2ae2303f120b933a1b4d2f405fe6f33/mercurial-7.0.3-cp313-cp313-win32.whl", hash = "sha256:8430814eea2bb7d345e848577b2293da3df58f9eefa681d64fcc8274823ada1e", size = 6754553, upload-time = "2025-07-15T18:42:31.234Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1c/73b2edf090450d5da78f174d5f9680123bb84573b480dc0d0616bcc35d42/mercurial-7.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a50ccca1f0ccee6378961f89eec87a385f0fe05479c3e3255cde5b86243b45fb", size = 7148554, upload-time = "2025-07-15T18:42:33.402Z" }, - { url = "https://files.pythonhosted.org/packages/5b/14/903e6bbf96b642d6b6cac1db806d35a1140a95b16a9be106651dfe1d3604/mercurial-7.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:f76515bbcee4d050cba54139f2775ad76bb31ff7d9ac9b284c72708f5497df82", size = 7113093, upload-time = "2025-07-15T18:42:36.235Z" }, - { url = "https://files.pythonhosted.org/packages/8c/40/4165dacac9cbbfadb28f5142ff58ef8460294561c42b8cef273467c98976/mercurial-7.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8f47c9dd700ea98b59596968cc50bc44fb4ec3645b96bd26724a0244d1cf2dee", size = 5228819, upload-time = "2025-07-15T18:42:07.73Z" }, - { url = "https://files.pythonhosted.org/packages/3e/54/9bd594e15689a72ff6de80bfae99539cc0b41e6b95e63656f5e0d702c545/mercurial-7.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:160182fe9a9f927d39fa168ceffdab27f951ab0c08f8e3121b9df75cf0ad6910", size = 7430403, upload-time = "2025-07-15T18:40:32.401Z" }, - { url = "https://files.pythonhosted.org/packages/be/dd/836298d1519a864445b482984e7b5818323ba5fbc57da5af1cfb4a4bfd80/mercurial-7.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ceebe9ed03cba42e0c8e2e34b6e701f87526eb8992c407b591660cb61b30030", size = 7124189, upload-time = "2025-07-15T18:40:34.014Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fd/59bfd379b9b6ccd9edf7a343cfe461f8758d6aaff5450785150b59a4b31e/mercurial-7.0.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:8feb37515a36a22a1bf0bc8fc6aaf060db8413e26ce72c46ea777fe2fb06a032", size = 7087703, upload-time = "2025-07-15T18:40:36.19Z" }, - { url = "https://files.pythonhosted.org/packages/25/58/6762ea7cdb98bb94796b029940f095691936f1b97426b43ba3acd3fb1ad5/mercurial-7.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:95659dd4f44866d7851d6984e685b89436f46a32d6acb6307bed6d0bbbde608d", size = 7313192, upload-time = "2025-07-15T18:40:38.328Z" }, - { url = "https://files.pythonhosted.org/packages/87/f9/195ce2d4db20dc538b812777923b3201fb2431c4df94f03fa61e932dbc28/mercurial-7.0.3-cp38-cp38-win32.whl", hash = "sha256:f1d476f869bb8970749c942fb088ac55058f2ff96b200cc06ee25ab0f93b6f66", size = 6346281, upload-time = "2025-07-15T18:42:37.98Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a6/ca515684ff7f128c4abd079de703ca105c70cd22501f902f0d809a224ada/mercurial-7.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5de4e1857cf3cce70e62c1ab5df69338d0458f252c6535fe8252733306cb47f4", size = 6517260, upload-time = "2025-07-15T18:42:39.95Z" }, - { url = "https://files.pythonhosted.org/packages/74/8d/c5ece4176f2fab81c7fd49042beacd0040be6e671509bcea3ab2656de356/mercurial-7.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b144d08831a8307892f80f91d91eddc81047e48ad2606e74a7fa62ae374d793f", size = 5229032, upload-time = "2025-07-15T18:42:09.47Z" }, - { url = "https://files.pythonhosted.org/packages/ed/07/031c13988db97de7587459509ac5bb1202c660dcf866c4e012567d08e21b/mercurial-7.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95d26966941f78fcc8eb5c27f98eaa5cc7328e589387318c50898a007196a139", size = 7417123, upload-time = "2025-07-15T18:40:40.037Z" }, - { url = "https://files.pythonhosted.org/packages/04/4e/b680b7743281c2f5265402bb0ac75665b648c5f9a7e35f539bc49ced2919/mercurial-7.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:321d34f0581fbe08cb86ea90160e6e1fc1118259e54d31a22a8d789714455c05", size = 7113120, upload-time = "2025-07-15T18:40:41.791Z" }, - { url = "https://files.pythonhosted.org/packages/55/f7/dbec235b414888ce4447ec8a190dff9ef39b05320cad107bfb234011e6c9/mercurial-7.0.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b480ddd01ea97a37cde1eee695041dd26968cc7b5b1ac309eb70a7b7f6eb6733", size = 7079437, upload-time = "2025-07-15T18:40:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/56/a9/e8d135eebf9998c6cedc47dec0f170c57ef424f4461fc031e1d5df5b4de5/mercurial-7.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:177d182aac2a25155ad561456db38865a9ff8e4b9c360992c33297908d30073f", size = 7304801, upload-time = "2025-07-15T18:40:45.639Z" }, - { url = "https://files.pythonhosted.org/packages/c3/78/011f6dde3884fc6733a88e97c86ba3efae5925c0b8d0a3759ef54327c1e3/mercurial-7.0.3-cp39-cp39-win32.whl", hash = "sha256:76c8d48ca3789b5f8c706600a458e6c9d7f32de2ce865dffee70a88b55ae0805", size = 6501011, upload-time = "2025-07-15T18:42:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/e6/08/61bf6699cc067d271ce274f9e88073807d57895f658311caa3e7e39ee1d2/mercurial-7.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:5e14a3232467bbff0efb899ade5540c34712044f0b00d3d8ea40e5c1172e5ab0", size = 6660402, upload-time = "2025-07-15T18:42:45.918Z" }, - { url = "https://files.pythonhosted.org/packages/0c/80/1dcef621cb69d45cb2a7a9e06c68be8044bc604817d19eabeb34185a3874/mercurial-7.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:c816077bffbb2018c32c867036b6f36841674431b781a42740430d400f76a3d4", size = 6616695, upload-time = "2025-07-15T18:42:47.814Z" }, -] - [[package]] name = "mergedeep" version = "1.3.4" @@ -1647,7 +1592,6 @@ rich = [ ] test = [ { name = "build" }, - { name = "mercurial" }, { 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'" }, @@ -1661,7 +1605,6 @@ test = [ [package.metadata] requires-dist = [ { name = "build", marker = "extra == 'test'" }, - { name = "mercurial", marker = "extra == 'test'" }, { name = "mkdocs", marker = "extra == 'docs'" }, { name = "mkdocs-entangled-plugin", marker = "extra == 'docs'" }, { name = "mkdocs-include-markdown-plugin", marker = "extra == 'docs'" },