From 263dc5bc079e8a58f54297f14f6a9e4d242d2393 Mon Sep 17 00:00:00 2001 From: Joshua Date: Thu, 18 Jan 2024 11:44:24 -0600 Subject: [PATCH 001/162] a few fixes and tests --- src/setuptools_scm/_config.py | 9 +++++---- src/setuptools_scm/_entrypoints.py | 31 ++++++++---------------------- src/setuptools_scm/version.py | 25 +++++++++++++++--------- testing/test_config.py | 11 +++++++++++ testing/test_version.py | 31 ++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 36 deletions(-) diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 5e5feb17..3db74739 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -41,8 +41,8 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]: group_names = regex.groupindex.keys() if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names): - warnings.warn( - "Expected tag_regex to contain a single match group or a group named" + raise ValueError( + f"Expected tag_regex '{regex.pattern}' to contain a single match group or a group named" " 'version' to identify the version part of any tag." ) @@ -105,6 +105,9 @@ class Configuration: parent: _t.PathT | None = None + def __post_init__(self): + self.tag_regex = _check_tag_regex(self.tag_regex) + @property def absolute_root(self) -> str: return _check_absolute_root(self.root, self.relative_to) @@ -139,13 +142,11 @@ def from_data( given configuration data create a config instance after validating tag regex/version class """ - tag_regex = _check_tag_regex(data.pop("tag_regex", None)) version_cls = _validate_version_cls( data.pop("version_cls", None), data.pop("normalize", True) ) return cls( relative_to, version_cls=version_cls, - tag_regex=tag_regex, **data, ) diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index 50c91829..7f443e2a 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -5,7 +5,6 @@ from typing import Callable from typing import cast from typing import Iterator -from typing import overload from typing import TYPE_CHECKING from . import _log @@ -106,34 +105,20 @@ def _iter_version_schemes( yield scheme_value -@overload def _call_version_scheme( version: version.ScmVersion, entrypoint: str, given_value: _t.VERSION_SCHEMES, - default: str, -) -> str: - ... - - -@overload -def _call_version_scheme( - version: version.ScmVersion, - entrypoint: str, - given_value: _t.VERSION_SCHEMES, - default: None, -) -> str | None: - ... - - -def _call_version_scheme( - version: version.ScmVersion, - entrypoint: str, - given_value: _t.VERSION_SCHEMES, - default: str | None, + default: str | None = None, ) -> str | None: + found_any_implementation = False for scheme in _iter_version_schemes(entrypoint, given_value): + found_any_implementation = True result = scheme(version) if result is not None: return result - return default + if not found_any_implementation: + raise ValueError(f'Couldn\'t find any implementations for entrypoint "{entrypoint}" with value "{given_value}".') + if default is not None: + return default + raise ValueError(f'None of the "{entrypoint}" entrypoints matching "{given_value}" returned a value.') \ No newline at end of file diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index f43e14b6..013d982b 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -55,15 +55,21 @@ def _parse_version_tag( log.debug( "key %s data %s, %s, %r", key, match.groupdict(), match.groups(), full ) - result = _TagDict( - version=match.group(key), - prefix=full[: match.start(key)], - suffix=full[match.end(key) :], - ) - log.debug("tag %r parsed to %r", tag, result) - assert result["version"] - return result + if version := match.group(key): + result = _TagDict( + version=version, + prefix=full[: match.start(key)], + suffix=full[match.end(key) :], + ) + + log.debug("tag %r parsed to %r", tag, result) + return result + + raise ValueError( + f'The tag_regex "{config.tag_regex.pattern}" matched tag "{tag}", however the matched' + ' group has no value.' + ) else: log.debug("tag %r did not parse", tag) @@ -428,8 +434,9 @@ def format_version(version: ScmVersion) -> str: if version.preformatted: assert isinstance(version.tag, str) return version.tag + main_version = _entrypoints._call_version_scheme( - version, "setuptools_scm.version_scheme", version.config.version_scheme, None + version, "setuptools_scm.version_scheme", version.config.version_scheme ) log.debug("version %s", main_version) assert main_version is not None diff --git a/testing/test_config.py b/testing/test_config.py index 6f19b23b..c148d174 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -97,3 +97,14 @@ def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> No assert pristine.root != overridden.root assert pristine.fallback_root != overridden.fallback_root + +@pytest.mark.parametrize( + "tag_regex", [ + r".*", + r"(.+)(.+)", + r"((.*))", + ] +) +def test_config_bad_regex(tag_regex: str) -> None: + with pytest.raises(ValueError, match=f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match group or a group named 'version' to identify the version part of any tag."): + Configuration(tag_regex=tag_regex) diff --git a/testing/test_version.py b/testing/test_version.py index ea4c7d99..c2d60154 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -189,6 +189,10 @@ def test_tag_regex1(tag: str, expected: str) -> None: assert result.tag.public == expected +def test_regex_match_but_no_version() -> None: + with pytest.raises(ValueError, match=r'The tag_regex "\(\?P\)\.\*" matched tag "v1", however the matched group has no value'): + meta("v1", config=replace(c, tag_regex="(?P).*")) + @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471") def test_version_bump_bad() -> None: class YikesVersion: @@ -410,3 +414,30 @@ def __repr__(self) -> str: assert isinstance(scm_version.tag, MyVersion) assert str(scm_version.tag) == "Custom 1.0.0-foo" + +@pytest.mark.parametrize( + "config_key", ["version_scheme", "local_scheme"] +) +def test_no_matching_entrypoints(config_key: str) -> None: + version = meta( + "1.0", + config=replace( + c, + **{ + config_key:"nonexistant" + } + ), + ) + with pytest.raises(ValueError, match=r'Couldn\'t find any implementations for entrypoint "setuptools_scm\..*?" with value "nonexistant"'): + format_version(version) + +def test_all_entrypoints_return_none() -> None: + version = meta( + "1.0", + config=replace( + c, + version_scheme=lambda v: None, + ), + ) + with pytest.raises(ValueError, match=r'None of the "setuptools_scm.version_scheme" entrypoints matching .*? returned a value.'): + format_version(version) From 4497912995b248f18f74d31b88351ee7fed9e90e Mon Sep 17 00:00:00 2001 From: Joshua Date: Thu, 18 Jan 2024 11:49:15 -0600 Subject: [PATCH 002/162] newline --- src/setuptools_scm/_entrypoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index 7f443e2a..e126f8a7 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -121,4 +121,4 @@ def _call_version_scheme( raise ValueError(f'Couldn\'t find any implementations for entrypoint "{entrypoint}" with value "{given_value}".') if default is not None: return default - raise ValueError(f'None of the "{entrypoint}" entrypoints matching "{given_value}" returned a value.') \ No newline at end of file + raise ValueError(f'None of the "{entrypoint}" entrypoints matching "{given_value}" returned a value.') From 90810a3a7c0edeee19bdbcad625794a98ee843b3 Mon Sep 17 00:00:00 2001 From: Joshua Date: Thu, 18 Jan 2024 11:50:27 -0600 Subject: [PATCH 003/162] space --- testing/test_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_version.py b/testing/test_version.py index c2d60154..da4aebf6 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -424,7 +424,7 @@ def test_no_matching_entrypoints(config_key: str) -> None: config=replace( c, **{ - config_key:"nonexistant" + config_key: "nonexistant" } ), ) From d9b4d24d3278fa1d23bb2942859e5c7acdf4642b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:52:08 +0000 Subject: [PATCH 004/162] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/setuptools_scm/_entrypoints.py | 8 ++++++-- src/setuptools_scm/version.py | 2 +- testing/test_config.py | 17 +++++++++++------ testing/test_version.py | 29 +++++++++++++++++------------ 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index e126f8a7..9c182c00 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -118,7 +118,11 @@ def _call_version_scheme( if result is not None: return result if not found_any_implementation: - raise ValueError(f'Couldn\'t find any implementations for entrypoint "{entrypoint}" with value "{given_value}".') + raise ValueError( + f'Couldn\'t find any implementations for entrypoint "{entrypoint}" with value "{given_value}".' + ) if default is not None: return default - raise ValueError(f'None of the "{entrypoint}" entrypoints matching "{given_value}" returned a value.') + raise ValueError( + f'None of the "{entrypoint}" entrypoints matching "{given_value}" returned a value.' + ) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 013d982b..afddf74a 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -68,7 +68,7 @@ def _parse_version_tag( raise ValueError( f'The tag_regex "{config.tag_regex.pattern}" matched tag "{tag}", however the matched' - ' group has no value.' + " group has no value." ) else: log.debug("tag %r did not parse", tag) diff --git a/testing/test_config.py b/testing/test_config.py index c148d174..fd7b66af 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -98,13 +98,18 @@ def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> No assert pristine.root != overridden.root assert pristine.fallback_root != overridden.fallback_root + @pytest.mark.parametrize( - "tag_regex", [ - r".*", - r"(.+)(.+)", - r"((.*))", - ] + "tag_regex", + [ + r".*", + r"(.+)(.+)", + r"((.*))", + ], ) def test_config_bad_regex(tag_regex: str) -> None: - with pytest.raises(ValueError, match=f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match group or a group named 'version' to identify the version part of any tag."): + with pytest.raises( + ValueError, + match=f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match group or a group named 'version' to identify the version part of any tag.", + ): Configuration(tag_regex=tag_regex) diff --git a/testing/test_version.py b/testing/test_version.py index da4aebf6..4b17d930 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -190,9 +190,13 @@ def test_tag_regex1(tag: str, expected: str) -> None: def test_regex_match_but_no_version() -> None: - with pytest.raises(ValueError, match=r'The tag_regex "\(\?P\)\.\*" matched tag "v1", however the matched group has no value'): + with pytest.raises( + ValueError, + match=r'The tag_regex "\(\?P\)\.\*" matched tag "v1", however the matched group has no value', + ): meta("v1", config=replace(c, tag_regex="(?P).*")) + @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471") def test_version_bump_bad() -> None: class YikesVersion: @@ -415,22 +419,20 @@ def __repr__(self) -> str: assert isinstance(scm_version.tag, MyVersion) assert str(scm_version.tag) == "Custom 1.0.0-foo" -@pytest.mark.parametrize( - "config_key", ["version_scheme", "local_scheme"] -) + +@pytest.mark.parametrize("config_key", ["version_scheme", "local_scheme"]) def test_no_matching_entrypoints(config_key: str) -> None: version = meta( "1.0", - config=replace( - c, - **{ - config_key: "nonexistant" - } - ), + config=replace(c, **{config_key: "nonexistant"}), ) - with pytest.raises(ValueError, match=r'Couldn\'t find any implementations for entrypoint "setuptools_scm\..*?" with value "nonexistant"'): + with pytest.raises( + ValueError, + match=r'Couldn\'t find any implementations for entrypoint "setuptools_scm\..*?" with value "nonexistant"', + ): format_version(version) + def test_all_entrypoints_return_none() -> None: version = meta( "1.0", @@ -439,5 +441,8 @@ def test_all_entrypoints_return_none() -> None: version_scheme=lambda v: None, ), ) - with pytest.raises(ValueError, match=r'None of the "setuptools_scm.version_scheme" entrypoints matching .*? returned a value.'): + with pytest.raises( + ValueError, + match=r'None of the "setuptools_scm.version_scheme" entrypoints matching .*? returned a value.', + ): format_version(version) From cdcc699055eae3eba3868beaf9cf5272745a4871 Mon Sep 17 00:00:00 2001 From: Joshua Date: Thu, 18 Jan 2024 12:19:51 -0600 Subject: [PATCH 005/162] pre-commit --- src/setuptools_scm/_config.py | 6 +++--- src/setuptools_scm/_entrypoints.py | 8 +++++--- src/setuptools_scm/version.py | 4 ++-- testing/test_config.py | 8 ++++++-- testing/test_version.py | 22 ++++++++++++++++------ 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 3db74739..fd19993c 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -42,8 +42,8 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]: group_names = regex.groupindex.keys() if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names): raise ValueError( - f"Expected tag_regex '{regex.pattern}' to contain a single match group or a group named" - " 'version' to identify the version part of any tag." + f"Expected tag_regex '{regex.pattern}' to contain a single match group or" + " a group named 'version' to identify the version part of any tag." ) return regex @@ -105,7 +105,7 @@ class Configuration: parent: _t.PathT | None = None - def __post_init__(self): + def __post_init__(self) -> None: self.tag_regex = _check_tag_regex(self.tag_regex) @property diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index 9c182c00..81f623e2 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -110,7 +110,7 @@ def _call_version_scheme( entrypoint: str, given_value: _t.VERSION_SCHEMES, default: str | None = None, -) -> str | None: +) -> str: found_any_implementation = False for scheme in _iter_version_schemes(entrypoint, given_value): found_any_implementation = True @@ -119,10 +119,12 @@ def _call_version_scheme( return result if not found_any_implementation: raise ValueError( - f'Couldn\'t find any implementations for entrypoint "{entrypoint}" with value "{given_value}".' + f'Couldn\'t find any implementations for entrypoint "{entrypoint}"' + f' with value "{given_value}".' ) if default is not None: return default raise ValueError( - f'None of the "{entrypoint}" entrypoints matching "{given_value}" returned a value.' + f'None of the "{entrypoint}" entrypoints matching "{given_value}"' + " returned a value." ) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index afddf74a..a47465e7 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -67,8 +67,8 @@ def _parse_version_tag( return result raise ValueError( - f'The tag_regex "{config.tag_regex.pattern}" matched tag "{tag}", however the matched' - " group has no value." + f'The tag_regex "{config.tag_regex.pattern}" matched tag "{tag}", ' + "however the matched group has no value." ) else: log.debug("tag %r did not parse", tag) diff --git a/testing/test_config.py b/testing/test_config.py index fd7b66af..fd0eeaeb 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -110,6 +110,10 @@ def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> No def test_config_bad_regex(tag_regex: str) -> None: with pytest.raises( ValueError, - match=f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match group or a group named 'version' to identify the version part of any tag.", + match=( + f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match" + " group or a group named 'version' to identify the version part of any" + " tag." + ), ): - Configuration(tag_regex=tag_regex) + Configuration(tag_regex=re.compile(tag_regex)) diff --git a/testing/test_version.py b/testing/test_version.py index 4b17d930..95a0a460 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from dataclasses import replace from datetime import date from datetime import timedelta @@ -192,9 +193,12 @@ def test_tag_regex1(tag: str, expected: str) -> None: def test_regex_match_but_no_version() -> None: with pytest.raises( ValueError, - match=r'The tag_regex "\(\?P\)\.\*" matched tag "v1", however the matched group has no value', + match=( + r'The tag_regex "\(\?P\)\.\*" matched tag "v1", ' + " however the matched group has no value" + ), ): - meta("v1", config=replace(c, tag_regex="(?P).*")) + meta("v1", config=replace(c, tag_regex=re.compile("(?P).*"))) @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471") @@ -424,11 +428,14 @@ def __repr__(self) -> str: def test_no_matching_entrypoints(config_key: str) -> None: version = meta( "1.0", - config=replace(c, **{config_key: "nonexistant"}), + config=replace(c, **{config_key: "nonexistant"}), # type: ignore ) with pytest.raises( ValueError, - match=r'Couldn\'t find any implementations for entrypoint "setuptools_scm\..*?" with value "nonexistant"', + match=( + r'Couldn\'t find any implementations for entrypoint "setuptools_scm\..*?"' + ' with value "nonexistant"' + ), ): format_version(version) @@ -438,11 +445,14 @@ def test_all_entrypoints_return_none() -> None: "1.0", config=replace( c, - version_scheme=lambda v: None, + version_scheme=lambda v: None, # type: ignore ), ) with pytest.raises( ValueError, - match=r'None of the "setuptools_scm.version_scheme" entrypoints matching .*? returned a value.', + match=( + 'None of the "setuptools_scm.version_scheme" entrypoints matching' + r" .*? returned a value." + ), ): format_version(version) From 5f20c3ed9de83d7cbcf237368d8faedcb09764d2 Mon Sep 17 00:00:00 2001 From: Joshua Date: Thu, 18 Jan 2024 12:30:59 -0600 Subject: [PATCH 006/162] fix bad string --- testing/test_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_version.py b/testing/test_version.py index 95a0a460..57298f43 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -194,7 +194,7 @@ def test_regex_match_but_no_version() -> None: with pytest.raises( ValueError, match=( - r'The tag_regex "\(\?P\)\.\*" matched tag "v1", ' + r'The tag_regex "\(\?P\)\.\*" matched tag "v1",' " however the matched group has no value" ), ): From 47b9b4d97a454e469b8592a586b1fed37ec8febf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 18:07:12 +0000 Subject: [PATCH 007/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.6 → v0.11.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.6...v0.11.7) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a42e2d10..5132bc6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.6 + rev: v0.11.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] From 914029d5bcd208f0487a1e3b4818ffda9b9bb5ad Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 4 May 2025 14:27:11 +0000 Subject: [PATCH 008/162] fix: update customizing.md to fix missing import --- docs/customizing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/customizing.md b/docs/customizing.md index 616e12e9..577da0cd 100644 --- a/docs/customizing.md +++ b/docs/customizing.md @@ -55,7 +55,7 @@ dynamic = [ With the pep 517/518 build backend, setuptools-scm is importable from `setup.py` ``` { .python title="setup.py" } -import setuptools +from setuptools import setup from setuptools_scm.version import get_local_dirty_tag def clean_scheme(version): From aeb97b9f0871ae40408372368c976c8977c38705 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 18:14:56 +0000 Subject: [PATCH 009/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.7 → v0.11.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.7...v0.11.8) - [github.com/scientific-python/cookie: 2025.01.22 → 2025.05.02](https://github.com/scientific-python/cookie/compare/2025.01.22...2025.05.02) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5132bc6c..525c5b4c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.7 + rev: v0.11.8 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] @@ -28,6 +28,6 @@ repos: - rich - repo: https://github.com/scientific-python/cookie - rev: 2025.01.22 + rev: 2025.05.02 hooks: - id: sp-repo-review From 91898e947114e2498db8a7967fc2c24ce75df99d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:10:48 +0000 Subject: [PATCH 010/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.8 → v0.11.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.8...v0.11.13) - [github.com/pre-commit/mirrors-mypy: v1.15.0 → v1.16.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.15.0...v1.16.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 525c5b4c..ccc361c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 + rev: v0.11.13 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.16.0 hooks: - id: mypy args: [--strict] From 0b477b9d65052584c098808d9755a2797f7994b0 Mon Sep 17 00:00:00 2001 From: Manuel Jacob Date: Wed, 11 Jun 2025 17:49:13 +0200 Subject: [PATCH 011/162] chore: remove unused class attribute --- src/setuptools_scm/hg_git.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index 9cab6f45..ea158a3f 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -24,8 +24,6 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir): - COMMAND = "hg" - @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None: require_command("hg") From 0622511a3fc8127d6896ca970b2b1cbdd3a42546 Mon Sep 17 00:00:00 2001 From: Manuel Jacob Date: Wed, 11 Jun 2025 18:01:42 +0200 Subject: [PATCH 012/162] chore: remove redundant call to require_command() In 560a1cb5ff3a3c052e3641034476a3b0554edb86, the call to require_command() was moved from HgWorkdir.from_potential_worktree() to the caller. That caller is the only caller of GitWorkdirHgClient.from_potential_worktree() as well, so it is redundant and can be removed. --- src/setuptools_scm/hg_git.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index 9cab6f45..0bee03df 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -9,7 +9,6 @@ from . import _types as _t from ._run_cmd import CompletedProcess as _CompletedProcess -from ._run_cmd import require_command from ._run_cmd import run as _run from .git import GitWorkdir from .hg import HgWorkdir @@ -28,7 +27,6 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir): @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None: - require_command("hg") res = _run(["hg", "root"], cwd=wd).parse_success(parse=Path) if res is None: return None From f3be9f7211baf534a19fca354a0e384f25a4f77b Mon Sep 17 00:00:00 2001 From: Manuel Jacob Date: Wed, 11 Jun 2025 21:20:54 +0200 Subject: [PATCH 013/162] chore: pass list instead of string to _run() This makes it consistent with the other invocations in this file and makes it easier to parametrize the Mercurial command in the future. --- src/setuptools_scm/hg_git.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index 6dc098fe..af669036 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -42,9 +42,9 @@ def get_branch(self) -> str | None: return res.stdout def get_head_date(self) -> date | None: - return _run('hg log -r . -T "{shortdate(date)}"', cwd=self.path).parse_success( - parse=date.fromisoformat, error_msg="head date err" - ) + return _run( + ["hg", "log", "-r", ".", "-T", "{shortdate(date)}"], cwd=self.path + ).parse_success(parse=date.fromisoformat, error_msg="head date err") def is_shallow(self) -> bool: return False @@ -53,7 +53,7 @@ def fetch_shallow(self) -> None: pass def get_hg_node(self) -> str | None: - res = _run('hg log -r . -T "{node}"', cwd=self.path) + res = _run(["hg", "log", "-r", ".", "-T", "{node}"], cwd=self.path) if res.returncode: return None else: From 2ac718c9f3b91a2058470d95e953c863e1b37702 Mon Sep 17 00:00:00 2001 From: Manuel Jacob Date: Wed, 11 Jun 2025 21:50:03 +0200 Subject: [PATCH 014/162] Make Mercurial command configurable by an environment variable. This is useful when e.g. developing Mercurial or Mercurial extensions. Previously, the first ``hg`` binary in PATH was used. If the Mercurial in the current virtual environment was broken, it was impossible to install anything that uses setuptools-scm to determine a version from Mercurial. With this change, it is possible to set the SETUPTOOLS_SCM_HG_COMMAND environment variable to the standard system-wide Mercurial executable. Also, it makes it possible to make setuptools-scm use chg, a variant of Mercurial that uses a daemon to save start-up overhead. Using it, the time of running ``uv pip install`` of a small-to-medium-size package decreased from 8.826s to 2.965s (a 3x reduction). If the environment variable is not set, the behavior remains unchanged. --- changelog.d/20250612_144312_me_hg_command.md | 4 +++ docs/config.md | 5 +++ src/setuptools_scm/_file_finders/hg.py | 6 ++-- src/setuptools_scm/hg.py | 12 ++++--- src/setuptools_scm/hg_git.py | 19 +++++----- testing/conftest.py | 9 +++++ testing/test_file_finder.py | 33 +++++++++++++++++ testing/test_hg_git.py | 38 ++++++++++++++++++++ testing/test_mercurial.py | 32 +++++++++++++++++ 9 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 changelog.d/20250612_144312_me_hg_command.md diff --git a/changelog.d/20250612_144312_me_hg_command.md b/changelog.d/20250612_144312_me_hg_command.md new file mode 100644 index 00000000..ff109f13 --- /dev/null +++ b/changelog.d/20250612_144312_me_hg_command.md @@ -0,0 +1,4 @@ +### Added + +- make Mercurial command configurable by environment variable `SETUPTOOLS_SCM_HG_COMMAND` + diff --git a/docs/config.md b/docs/config.md index b30fce86..fbeef5c4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -143,6 +143,11 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ : a ``os.pathsep`` separated list of directory names to ignore for root finding +`SETUPTOOLS_SCM_HG_COMMAND` +: command used for running Mercurial (defaults to ``hg``) + + for example, set this to ``chg`` to reduce start-up overhead of Mercurial + diff --git a/src/setuptools_scm/_file_finders/hg.py b/src/setuptools_scm/_file_finders/hg.py index 9115a5fa..4fc3a1ee 100644 --- a/src/setuptools_scm/_file_finders/hg.py +++ b/src/setuptools_scm/_file_finders/hg.py @@ -13,11 +13,13 @@ log = logging.getLogger(__name__) +HG_COMMAND = os.environ.get("SETUPTOOLS_SCM_HG_COMMAND", "hg") + def _hg_toplevel(path: str) -> str | None: try: return _run( - ["hg", "root"], + [HG_COMMAND, "root"], cwd=(path or "."), check=True, ).parse_success(norm_real) @@ -32,7 +34,7 @@ def _hg_toplevel(path: str) -> str | None: def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: hg_files: set[str] = set() hg_dirs = {toplevel} - res = _run(["hg", "files"], cwd=toplevel) + res = _run([HG_COMMAND, "files"], cwd=toplevel) if res.returncode: return set(), set() for name in res.stdout.splitlines(): diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index d8307c78..f1c1560d 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -23,11 +23,13 @@ log = logging.getLogger(__name__) +HG_COMMAND = os.environ.get("SETUPTOOLS_SCM_HG_COMMAND", "hg") + class HgWorkdir(Workdir): @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None: - res = _run(["hg", "root"], wd) + res = _run([HG_COMMAND, "root"], wd) if res.returncode: return None return cls(Path(res.stdout)) @@ -45,7 +47,7 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: # the dedicated class GitWorkdirHgClient) branch, dirty_str, dirty_date = _run( - ["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"], + [HG_COMMAND, "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"], cwd=self.path, check=True, ).stdout.split("\n") @@ -108,7 +110,7 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: return None def hg_log(self, revset: str, template: str) -> str: - cmd = ["hg", "log", "-r", revset, "-T", template] + cmd = [HG_COMMAND, "log", "-r", revset, "-T", template] return _run(cmd, cwd=self.path, check=True).stdout @@ -144,9 +146,9 @@ def check_changes_since_tag(self, tag: str | None) -> bool: def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: - _require_command("hg") + _require_command(HG_COMMAND) if os.path.exists(os.path.join(root, ".hg/git")): - res = _run(["hg", "path"], root) + res = _run([HG_COMMAND, "path"], root) if not res.returncode: for line in res.stdout.split("\n"): if line.startswith("default ="): diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index af669036..f2336e3c 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -11,6 +11,7 @@ from ._run_cmd import CompletedProcess as _CompletedProcess from ._run_cmd import run as _run from .git import GitWorkdir +from .hg import HG_COMMAND from .hg import HgWorkdir log = logging.getLogger(__name__) @@ -25,17 +26,17 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir): @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None: - res = _run(["hg", "root"], cwd=wd).parse_success(parse=Path) + res = _run([HG_COMMAND, "root"], cwd=wd).parse_success(parse=Path) if res is None: return None return cls(res) def is_dirty(self) -> bool: - res = _run(["hg", "id", "-T", "{dirty}"], cwd=self.path, check=True) + res = _run([HG_COMMAND, "id", "-T", "{dirty}"], cwd=self.path, check=True) return bool(res.stdout) def get_branch(self) -> str | None: - res = _run(["hg", "id", "-T", "{bookmarks}"], cwd=self.path) + res = _run([HG_COMMAND, "id", "-T", "{bookmarks}"], cwd=self.path) if res.returncode: log.info("branch err %s", res) return None @@ -43,7 +44,7 @@ def get_branch(self) -> str | None: def get_head_date(self) -> date | None: return _run( - ["hg", "log", "-r", ".", "-T", "{shortdate(date)}"], cwd=self.path + [HG_COMMAND, "log", "-r", ".", "-T", "{shortdate(date)}"], cwd=self.path ).parse_success(parse=date.fromisoformat, error_msg="head date err") def is_shallow(self) -> bool: @@ -53,7 +54,7 @@ def fetch_shallow(self) -> None: pass def get_hg_node(self) -> str | None: - res = _run(["hg", "log", "-r", ".", "-T", "{node}"], cwd=self.path) + res = _run([HG_COMMAND, "log", "-r", ".", "-T", "{node}"], cwd=self.path) if res.returncode: return None else: @@ -77,7 +78,7 @@ def node(self) -> str | None: if git_node is None: # trying again after hg -> git - _run(["hg", "gexport"], cwd=self.path) + _run([HG_COMMAND, "gexport"], cwd=self.path) git_node = self._hg2git(hg_node) if git_node is None: @@ -92,7 +93,7 @@ def node(self) -> str | None: return git_node[:7] def count_all_nodes(self) -> int: - res = _run(["hg", "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) + res = _run([HG_COMMAND, "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) return len(res.stdout) def default_describe(self) -> _CompletedProcess: @@ -104,7 +105,7 @@ def default_describe(self) -> _CompletedProcess: """ res = _run( [ - "hg", + HG_COMMAND, "log", "-r", "(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))", @@ -132,7 +133,7 @@ def default_describe(self) -> _CompletedProcess: logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags) return _FAKE_GIT_DESCRIBE_ERROR - res = _run(["hg", "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path) + res = _run([HG_COMMAND, "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path) if res.returncode: return _FAKE_GIT_DESCRIBE_ERROR distance = len(res.stdout) - 1 diff --git a/testing/conftest.py b/testing/conftest.py index 09b69c1a..ec936f7c 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -2,6 +2,7 @@ import contextlib import os +import shutil import sys from pathlib import Path @@ -86,6 +87,14 @@ def wd(tmp_path: Path) -> WorkDir: return WorkDir(target_wd) +@pytest.fixture(scope="session") +def hg_exe() -> str: + hg = shutil.which("hg") + if hg is None: + pytest.skip("hg executable not found") + return hg + + @pytest.fixture def repositories_hg_git(tmp_path: Path) -> tuple[WorkDir, WorkDir]: tmp_path = tmp_path.resolve() diff --git a/testing/test_file_finder.py b/testing/test_file_finder.py index 5902d8e5..9daf9b04 100644 --- a/testing/test_file_finder.py +++ b/testing/test_file_finder.py @@ -1,5 +1,6 @@ from __future__ import annotations +import importlib import os import sys @@ -8,6 +9,7 @@ import pytest from setuptools_scm._file_finders import find_files +from setuptools_scm._file_finders import hg from .wd_wrapper import WorkDir @@ -245,3 +247,34 @@ def test_archive( os.link("data/datafile", datalink) assert set(find_files()) == _sep({archive_file, "data/datafile", "data/datalink"}) + + +@pytest.fixture +def hg_wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir: + try: + wd("hg init") + except OSError: + pytest.skip("hg executable not found") + (wd.cwd / "file").touch() + wd("hg add file") + monkeypatch.chdir(wd.cwd) + return wd + + +def test_hg_gone(hg_wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PATH", str(hg_wd.cwd / "not-existing")) + assert set(find_files()) == set() + + +def test_hg_command_from_env( + hg_wd: WorkDir, + monkeypatch: pytest.MonkeyPatch, + request: pytest.FixtureRequest, + hg_exe: str, +) -> None: + with monkeypatch.context() as m: + m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) + m.setenv("PATH", str(hg_wd.cwd / "not-existing")) + request.addfinalizer(lambda: importlib.reload(hg)) + importlib.reload(hg) + assert set(find_files()) == {"file"} diff --git a/testing/test_hg_git.py b/testing/test_hg_git.py index 9527cb02..f2a9539f 100644 --- a/testing/test_hg_git.py +++ b/testing/test_hg_git.py @@ -1,9 +1,16 @@ from __future__ import annotations +import importlib + import pytest +from setuptools_scm import Configuration +from setuptools_scm import hg +from setuptools_scm import hg_git +from setuptools_scm._run_cmd import CommandNotFoundError from setuptools_scm._run_cmd import has_command from setuptools_scm._run_cmd import run +from setuptools_scm.hg import parse from testing.wd_wrapper import WorkDir @@ -81,3 +88,34 @@ def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None: wd("hg pull -u") assert wd_git.get_version() == "17.33.0rc0" assert wd.get_version() == "17.33.0rc0" + + +def test_hg_gone( + repositories_hg_git: tuple[WorkDir, WorkDir], monkeypatch: pytest.MonkeyPatch +) -> None: + wd = repositories_hg_git[0] + monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) + config = Configuration() + wd.write("pyproject.toml", "[tool.setuptools_scm]") + with pytest.raises(CommandNotFoundError, match=r"hg"): + parse(wd.cwd, config=config) + + assert wd.get_version(fallback_version="1.0") == "1.0" + + +def test_hg_command_from_env( + repositories_hg_git: tuple[WorkDir, WorkDir], + monkeypatch: pytest.MonkeyPatch, + request: pytest.FixtureRequest, + hg_exe: str, +) -> None: + wd = repositories_hg_git[0] + with monkeypatch.context() as m: + m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) + m.setenv("PATH", str(wd.cwd / "not-existing")) + request.addfinalizer(lambda: importlib.reload(hg)) + request.addfinalizer(lambda: importlib.reload(hg_git)) + importlib.reload(hg) + importlib.reload(hg_git) + wd.write("pyproject.toml", "[tool.setuptools_scm]") + assert wd.get_version().startswith("0.1.dev0+") diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 57073716..6c2a137c 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -1,5 +1,6 @@ from __future__ import annotations +import importlib import os from pathlib import Path @@ -9,6 +10,7 @@ import setuptools_scm._file_finders from setuptools_scm import Configuration +from setuptools_scm import hg from setuptools_scm._run_cmd import CommandNotFoundError from setuptools_scm._run_cmd import has_command from setuptools_scm.hg import archival_to_version @@ -67,6 +69,36 @@ def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: assert wd.get_version(fallback_version="1.0") == "1.0" +def test_hg_command_from_env( + wd: WorkDir, + monkeypatch: pytest.MonkeyPatch, + request: pytest.FixtureRequest, + hg_exe: str, +) -> None: + with monkeypatch.context() as m: + m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) + m.setenv("PATH", str(wd.cwd / "not-existing")) + request.addfinalizer(lambda: importlib.reload(hg)) + importlib.reload(hg) + wd.write("pyproject.toml", "[tool.setuptools_scm]") + assert wd.get_version() == "0.0" + + +def test_hg_command_from_env_is_invalid( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest +) -> None: + with monkeypatch.context() as m: + m.setenv("SETUPTOOLS_SCM_HG_COMMAND", str(wd.cwd / "not-existing")) + request.addfinalizer(lambda: importlib.reload(hg)) + importlib.reload(hg) + config = Configuration() + wd.write("pyproject.toml", "[tool.setuptools_scm]") + with pytest.raises(CommandNotFoundError, match=r"hg"): + parse(wd.cwd, config=config) + + assert wd.get_version(fallback_version="1.0") == "1.0" + + def test_find_files_stop_at_root_hg( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: From 1cd14fc95738ab982775a2400f8846d7e6a64e6c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:16:54 +0000 Subject: [PATCH 015/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.13 → v0.12.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.13...v0.12.0) - [github.com/pre-commit/mirrors-mypy: v1.16.0 → v1.16.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.0...v1.16.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccc361c6..74330d8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.12.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.0 + rev: v1.16.1 hooks: - id: mypy args: [--strict] From 9af48600cdcd48226b76e0886e973d124a53029b Mon Sep 17 00:00:00 2001 From: JeukOh Date: Thu, 10 Jul 2025 23:36:52 +0900 Subject: [PATCH 016/162] update precommit rev --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccc361c6..74330d8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.12.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.0 + rev: v1.16.1 hooks: - id: mypy args: [--strict] From cefe3a90b41f904bd134831e1bfd34146efb6b7f Mon Sep 17 00:00:00 2001 From: JeukOh Date: Thu, 10 Jul 2025 23:37:07 +0900 Subject: [PATCH 017/162] add console_scripts entrypoint in pyproject --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b66940ee..fd8181b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,9 @@ toml = [ documentation = "https://setuptools-scm.readthedocs.io/" repository = "https://github.com/pypa/setuptools-scm/" +[project.entry-points.console_scripts] +setuptools-scm = "setuptools_scm._cli:main" + [project.entry-points."distutils.setup_keywords"] use_scm_version = "setuptools_scm._integration.setuptools:version_keyword" From ab893770fa9dcd0f2caaa4aa8ecf9c01eee15acd Mon Sep 17 00:00:00 2001 From: JeukOh Date: Thu, 10 Jul 2025 23:37:16 +0900 Subject: [PATCH 018/162] add pip in test optional deps for test in uv venv --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index fd8181b3..fd5745b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ rich = [ "rich", ] test = [ + "pip", "build", "pytest", "rich", From 2e74a8c52926c9bd471b7fdf3be08dda85e5a277 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:13:35 +0000 Subject: [PATCH 019/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.0 → v0.12.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.0...v0.12.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74330d8a..2a9b64db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.0 + rev: v0.12.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] From 338aa306ab6bdb57e5a84264add0617fef9a4c20 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 23 Jul 2025 23:58:09 +0200 Subject: [PATCH 020/162] reintroduce a python 3.9 entrypints shim --- pyproject.toml | 1 - src/setuptools_scm/_entrypoints.py | 29 ++++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd5745b7..ffbd9778 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,6 @@ dependencies = [ "setuptools", # >= 61", 'tomli>=1; python_version < "3.11"', 'typing-extensions; python_version < "3.10"', - 'importlib-metadata>=4.6; python_version < "3.10"', ] [project.optional-dependencies] docs = [ diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index 3333eb5c..23ee8ce4 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -21,22 +21,29 @@ from ._config import Configuration from ._config import ParseFunction - if sys.version_info[:2] < (3, 10): - import importlib_metadata as im - else: - from importlib import metadata as im - +from importlib import metadata as im log = _log.log.getChild("entrypoints") -def entry_points(**kw: Any) -> im.EntryPoints: - if sys.version_info[:2] < (3, 10): - import importlib_metadata as im - else: - import importlib.metadata as im +if sys.version_info[:2] < (3, 10): + + def entry_points(*, group: str, name: str | None = None) -> list[im.EntryPoint]: + # Python 3.9: entry_points() returns dict, need to handle filtering manually + + eps = im.entry_points() # Returns dict + + group_eps = eps.get(group, []) + if name is not None: + return [ep for ep in group_eps if ep.name == name] + return group_eps +else: - return im.entry_points(**kw) + def entry_points(*, group: str, name: str | None = None) -> im.EntryPoints: + kw = {"group": group} + if name is not None: + kw["name"] = name + return im.entry_points(**kw) def version_from_entrypoint( From 45ed1cbb42d0629deb72b69568e9e5414652f590 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 12:24:08 +0200 Subject: [PATCH 021/162] update changelog --- CHANGELOG.md | 18 ++++++++++++++++++ changelog.d/20250612_144312_me_hg_command.md | 4 ---- 2 files changed, 18 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/20250612_144312_me_hg_command.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c8823c7e..511a1059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## Unreleased + +### Added + +- add `setuptools-scm` console_scripts entry point to make the CLI directly executable +- make Mercurial command configurable by environment variable `SETUPTOOLS_SCM_HG_COMMAND` + +### Changed + +- add `pip` to test optional dependencies for improved uv venv compatibility +- migrate to selectable entrypoints for better extensibility +- improve typing for entry_points + +### Fixed + +- reintroduce Python 3.9 entrypoints shim for compatibility +- fix #1136: update customizing.md to fix missing import + ## v8.3.1 ### Fixed diff --git a/changelog.d/20250612_144312_me_hg_command.md b/changelog.d/20250612_144312_me_hg_command.md deleted file mode 100644 index ff109f13..00000000 --- a/changelog.d/20250612_144312_me_hg_command.md +++ /dev/null @@ -1,4 +0,0 @@ -### Added - -- make Mercurial command configurable by environment variable `SETUPTOOLS_SCM_HG_COMMAND` - From 5440b83a8122c38ffc5c3f7e4fb6a65181b6ff49 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 12:27:15 +0200 Subject: [PATCH 022/162] pre-commit update --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a9b64db..c840b357 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.3 + rev: v0.12.4 hooks: - - id: ruff + - id: ruff-check args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 + rev: v1.17.0 hooks: - id: mypy args: [--strict] From d7a9c830039aeee73530e03e2271bb4a3ea14bc3 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 12:52:38 +0200 Subject: [PATCH 023/162] fix #1145 - normalize git commit timestamp to utc --- CHANGELOG.md | 1 + src/setuptools_scm/_run_cmd.py | 16 +++++++++++----- src/setuptools_scm/git.py | 8 +++++++- testing/test_git.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 511a1059..bfbb9981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixed +- fix #1145: ensure GitWorkdir.get_head_date returns consistent UTC dates regardless of local timezone - reintroduce Python 3.9 entrypoints shim for compatibility - fix #1136: update customizing.md to fix missing import diff --git a/src/setuptools_scm/_run_cmd.py b/src/setuptools_scm/_run_cmd.py index 5d5ec15e..2dff6369 100644 --- a/src/setuptools_scm/_run_cmd.py +++ b/src/setuptools_scm/_run_cmd.py @@ -81,6 +81,16 @@ def parse_success( return parse(self.stdout) +KEEP_GIT_ENV = ( + "GIT_CEILING_DIRECTORIES", + "GIT_EXEC_PATH", + "GIT_SSH", + "GIT_SSH_COMMAND", + "GIT_AUTHOR_DATE", + "GIT_COMMITTER_DATE", +) + + def no_git_env(env: Mapping[str, str]) -> dict[str, str]: # adapted from pre-commit # Too many bugs dealing with environment variables and GIT: @@ -95,11 +105,7 @@ def no_git_env(env: Mapping[str, str]) -> dict[str, str]: if k.startswith("GIT_"): log.debug("%s: %s", k, v) return { - k: v - for k, v in env.items() - if not k.startswith("GIT_") - or k - in ("GIT_CEILING_DIRECTORIES", "GIT_EXEC_PATH", "GIT_SSH", "GIT_SSH_COMMAND") + k: v for k, v in env.items() if not k.startswith("GIT_") or k in KEEP_GIT_ENV } diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 5be2f89d..4773f49c 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -123,7 +123,13 @@ def parse_timestamp(timestamp_text: str) -> date | None: return None if sys.version_info < (3, 11) and timestamp_text.endswith("Z"): timestamp_text = timestamp_text[:-1] + "+00:00" - return datetime.fromisoformat(timestamp_text).date() + + # Convert to UTC to ensure consistent date regardless of local timezone + dt = datetime.fromisoformat(timestamp_text) + log.debug("dt: %s", dt) + dt_utc = dt.astimezone(timezone.utc).date() + log.debug("dt utc: %s", dt_utc) + return dt_utc res = run_git( [ diff --git a/testing/test_git.py b/testing/test_git.py index 9186b1a6..9ef1f14a 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -515,6 +515,36 @@ def test_git_getdate_git_2_45_0_plus( assert git_wd.get_head_date() == date(2024, 4, 30) +def test_git_getdate_timezone_consistency( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that get_head_date returns consistent UTC dates regardless of local timezone. + + This test forces a git commit with a timestamp that represents a time + after midnight in a positive timezone offset but still the previous day in UTC. + This is the exact scenario that was causing test failures in issue #1145. + """ + # Create a timestamp that's problematic: + # - In Europe/Berlin (UTC+2): 2025-06-12 00:30:00 (June 12th) + # - In UTC: 2025-06-11 22:30:00 (June 11th) + problematic_timestamp = "2025-06-12T00:30:00+02:00" + + # Force git to use this specific timestamp for the commit + monkeypatch.setenv("GIT_AUTHOR_DATE", problematic_timestamp) + monkeypatch.setenv("GIT_COMMITTER_DATE", problematic_timestamp) + + wd.commit_testfile() + + git_wd = git.GitWorkdir(wd.cwd) + result_date = git_wd.get_head_date() + + # The correct behavior is to return the UTC date (2025-06-11) + # If the bug is present, it would return the timezone-local date (2025-06-12) + expected_utc_date = date(2025, 6, 11) + + assert result_date == expected_utc_date + + @pytest.fixture def signed_commit_wd(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> WorkDir: if not has_command("gpg", args=["--version"], warn=False): From d9d8ed21aa9c8d9efb248a1be7ee4c9f9dafb15a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 18:25:20 +0200 Subject: [PATCH 024/162] Fix #687: Ensure calendar versioning tests use a consistent time contex - prevent failures around midnight in non-UTC timezones - Update `meta` function to accept a time parameter for consistent behaviour in tests --- CHANGELOG.md | 1 + src/setuptools_scm/version.py | 20 ++++++++++++++----- testing/test_version.py | 37 +++++++++++++++++++++++++++-------- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfbb9981..249bcce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Fixed - fix #1145: ensure GitWorkdir.get_head_date returns consistent UTC dates regardless of local timezone +- fix #687: ensure calendar versioning tests use consistent time context to prevent failures around midnight in non-UTC timezones - reintroduce Python 3.9 entrypoints shim for compatibility - fix #1136: update customizing.md to fix missing import diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 29803fcd..eb9dcf63 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -212,11 +212,12 @@ def meta( branch: str | None = None, config: _config.Configuration, node_date: date | None = None, + time: datetime | None = None, ) -> ScmVersion: parsed_version = _parse_tag(tag, preformatted, config) log.info("version %s -> %s", tag, parsed_version) assert parsed_version is not None, f"Can't parse version {tag}" - return ScmVersion( + scm_version = ScmVersion( parsed_version, distance=distance, node=node, @@ -226,6 +227,9 @@ def meta( config=config, node_date=node_date, ) + if time is not None: + scm_version = dataclasses.replace(scm_version, time=time) + return scm_version def guess_next_version(tag_version: ScmVersion) -> str: @@ -365,7 +369,11 @@ def guess_next_date_ver( head_date = node_date or today # compute patch if match is None: - tag_date = today + # For legacy non-date tags, always use patch=0 (treat as "other day") + # Use yesterday to ensure tag_date != head_date + from datetime import timedelta + + tag_date = head_date - timedelta(days=1) else: tag_date = ( datetime.strptime(match.group("date"), date_fmt) @@ -373,11 +381,13 @@ def guess_next_date_ver( .date() ) if tag_date == head_date: - patch = "0" if match is None else (match.group("patch") or "0") - patch = int(patch) + 1 + assert match is not None + # Same day as existing date tag - increment patch + patch = int(match.group("patch") or "0") + 1 else: + # Different day or legacy non-date tag - use patch 0 if tag_date > head_date and match is not None: - # warn on future times + # warn on future times (only for actual date tags, not legacy) warnings.warn( f"your previous tag ({tag_date}) is ahead your node date ({head_date})" ) diff --git a/testing/test_version.py b/testing/test_version.py index 32a65c0d..c7789df4 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -2,7 +2,9 @@ from dataclasses import replace from datetime import date +from datetime import datetime from datetime import timedelta +from datetime import timezone from typing import Any import pytest @@ -269,11 +271,14 @@ def test_custom_version_schemes() -> None: assert custom_computed == no_guess_dev_version(version) +# Fixed time for consistent test behavior across timezone boundaries +# This prevents issue #687 where tests failed around midnight in non-UTC timezones +_TEST_TIME = datetime(2023, 12, 15, 12, 0, 0, tzinfo=timezone.utc) + + def date_offset(base_date: date | None = None, days_offset: int = 0) -> date: if base_date is None: - from setuptools_scm.version import _source_epoch_or_utc_now - - base_date = _source_epoch_or_utc_now().date() + base_date = _TEST_TIME.date() return base_date - timedelta(days=days_offset) @@ -304,12 +309,23 @@ def date_to_str( id="leading 0s", ), pytest.param( - meta(date_to_str(days_offset=3), config=c_non_normalize, dirty=True), + meta( + date_to_str(days_offset=3), + config=c_non_normalize, + dirty=True, + time=_TEST_TIME, + ), date_to_str() + ".0.dev0", id="dirty other day", ), pytest.param( - meta(date_to_str(), config=c_non_normalize, distance=2, branch="default"), + meta( + date_to_str(), + config=c_non_normalize, + distance=2, + branch="default", + time=_TEST_TIME, + ), date_to_str() + ".1.dev2", id="normal branch", ), @@ -382,8 +398,8 @@ def test_calver_by_date(version: ScmVersion, expected_next: str) -> None: [ pytest.param(meta("1.0.0", config=c), "1.0.0", id="SemVer exact stays"), pytest.param( - meta("1.0.0", config=c_non_normalize, dirty=True), - "09.02.13.1.dev0", + meta("1.0.0", config=c_non_normalize, dirty=True, time=_TEST_TIME), + "23.12.15.0.dev0", id="SemVer dirty is replaced by date", marks=pytest.mark.filterwarnings("ignore:.*legacy version.*:UserWarning"), ), @@ -397,7 +413,12 @@ def test_calver_by_date_semver(version: ScmVersion, expected_next: str) -> None: def test_calver_by_date_future_warning() -> None: with pytest.warns(UserWarning, match="your previous tag*"): calver_by_date( - meta(date_to_str(days_offset=-2), config=c_non_normalize, distance=2) + meta( + date_to_str(days_offset=-2), + config=c_non_normalize, + distance=2, + time=_TEST_TIME, + ) ) From f62c605da26f934941056801f012e2bba248422b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 19:39:36 +0200 Subject: [PATCH 025/162] fix #1001: document more version schemes + add examples --- CHANGELOG.md | 1 + docs/extending.md | 54 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 249bcce7..de5412d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - fix #687: ensure calendar versioning tests use consistent time context to prevent failures around midnight in non-UTC timezones - reintroduce Python 3.9 entrypoints shim for compatibility - fix #1136: update customizing.md to fix missing import +- fix #1001: document the missing version schemes and add examples in the docs ## v8.3.1 diff --git a/docs/extending.md b/docs/extending.md index 66f1ffd4..3d903173 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -53,9 +53,41 @@ representing the version. `guess-next-dev (default)` : Automatically guesses the next development version (default). Guesses the upcoming release by incrementing the pre-release segment if present, - otherwise by incrementing the micro segment. Then appends :code:`.devN`. + otherwise by incrementing the micro segment. Then appends `.devN`. In case the tag ends with `.dev0` the version is not bumped - and custom `.devN` versions will trigger a error. + and custom `.devN` versions will trigger an error. + + **Examples:** + - Tag `1.0.0` → version `1.0.1.dev0` (if dirty or distance > 0) + - Tag `1.0.0` → version `1.0.0` (if exact match) + +`calver-by-date` +: Calendar versioning scheme that generates versions based on dates. + Uses the format `YY.MM.DD.patch` or `YYYY.MM.DD.patch` depending on the existing tag format. + If the commit is on the same date as the latest tag, increments the patch number. + Otherwise, uses the current date with patch 0. Supports branch-specific versioning + for release branches. + + **Examples:** + - Tag `23.01.15.0` on same day → version `23.01.15.1.devN` + - Tag `23.01.15.0` on different day (e.g., 2023-01-16) → version `23.01.16.0.devN` + - Tag `2023.01.15.0` → uses 4-digit year format for new versions + +`no-guess-dev` +: Does no next version guessing, just adds `.post1.devN`. + This is the recommended replacement for the deprecated `post-release` scheme. + + **Examples:** + - Tag `1.0.0` → version `1.0.0.post1.devN` (if distance > 0) + - Tag `1.0.0` → version `1.0.0` (if exact match) + +`only-version` +: Only use the version from the tag, as given. + + !!! warning "This means version is no longer pseudo unique per commit" + + **Examples:** + - Tag `1.0.0` → version `1.0.0` (always, regardless of distance or dirty state) `post-release (deprecated)` : Generates post release versions (adds `.postN`) @@ -64,6 +96,9 @@ representing the version. !!! warning "the recommended replacement is `no-guess-dev`" + **Examples:** + - Tag `1.0.0` → version `1.0.0.postN` (where N is the distance) + `python-simplified-semver` : Basic semantic versioning. @@ -73,6 +108,10 @@ representing the version. This scheme is not compatible with pre-releases. + **Examples:** + - Tag `1.0.0` on non-feature branch → version `1.0.1.devN` + - Tag `1.0.0` on feature branch → version `1.1.0.devN` + `release-branch-semver` : Semantic versioning for projects with release branches. The same as `guess-next-dev` (incrementing the pre-release or micro segment) @@ -81,14 +120,9 @@ representing the version. non-release branch, increments the minor segment and sets the micro segment to zero, then appends `.devN` -`no-guess-dev` -: Does no next version guessing, just adds `.post1.devN` - -`only-version` -: Only use the version from the tag, as given. - - !!! warning "This means version is no longer pseudo unique per commit" - + **Examples:** + - Tag `1.0.0` on release branch `release-1.0` → version `1.0.1.devN` + - Tag `1.0.0` on development branch → version `1.1.0.devN` ### `setuptools_scm.local_scheme` Configures how the local part of a version is rendered given a From b0704021581c4c1ea579c2e39b5a064d7580d71d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 19:45:57 +0200 Subject: [PATCH 026/162] fix #1115: explicitly document file finder behaviour --- CHANGELOG.md | 1 + README.md | 2 ++ docs/config.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 10 ++++++++ docs/usage.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de5412d9..dc76b583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - reintroduce Python 3.9 entrypoints shim for compatibility - fix #1136: update customizing.md to fix missing import - fix #1001: document the missing version schemes and add examples in the docs +- fix #1115: explicitly document file finder behaviour ## v8.3.1 diff --git a/README.md b/README.md index e1f06f82..7722ba44 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ files that are managed by the SCM Unwanted files must be excluded via `MANIFEST.in` or [configuring Git archive][git-archive-docs]. +> **⚠️ Important:** Installing setuptools-scm automatically enables a file finder that includes **all SCM-tracked files** in your source distributions. This can be surprising if you have development files tracked in Git/Mercurial that you don't want in your package. Use `MANIFEST.in` to exclude unwanted files. See the [documentation] for details. + ## `pyproject.toml` usage The preferred way to configure [setuptools-scm] is to author diff --git a/docs/config.md b/docs/config.md index fbeef5c4..8f6210bb 100644 --- a/docs/config.md +++ b/docs/config.md @@ -152,6 +152,66 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ +## automatic file inclusion + +!!! warning "Setuptools File Finder Integration" + + `setuptools-scm` automatically registers a setuptools file finder that includes all SCM-tracked files in source distributions. This behavior is **always active** when setuptools-scm is installed, regardless of whether you use it for versioning. + +**How it works:** + +`setuptools-scm` provides a `setuptools.file_finders` entry point that: + +1. Automatically discovers SCM-managed files (Git, Mercurial) +2. Includes them in source distributions (`python -m build --sdist`) +3. Works for `include_package_data = True` in package building + +**Entry point registration:** +```toml +[project.entry-points."setuptools.file_finders"] +setuptools_scm = "setuptools_scm._file_finders:find_files" +``` + +**Files included by default:** +- All files tracked by Git (`git ls-files`) +- All files tracked by Mercurial (`hg files`) +- Includes: source code, documentation, tests, config files, etc. +- Excludes: untracked files, files in `.gitignore`/`.hgignore` + +**Controlling inclusion:** + +Use `MANIFEST.in` to override the automatic behavior: + +```text title="MANIFEST.in" +# Exclude development files +exclude .pre-commit-config.yaml +exclude tox.ini +global-exclude *.pyc __pycache__/ + +# Exclude entire directories +prune docs/ +prune testing/ + +# Include non-SCM files +include data/important.json +``` + +**Debugging file inclusion:** + +```bash +# List files that will be included +python -m setuptools_scm ls + +# Build and inspect sdist contents +python -m build --sdist +tar -tzf dist/package-*.tar.gz +``` + +!!! note "Cannot be disabled" + + The file finder cannot be disabled through configuration - it's automatically active when setuptools-scm is installed. If you need to disable it completely, you must remove setuptools-scm from your build environment (which also means you can't use it for versioning). + + ## api reference ### constants diff --git a/docs/index.md b/docs/index.md index b40dbf42..29a420fc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,6 +10,16 @@ files that are managed by the SCM Unwanted files must be excluded via `MANIFEST.in` or [configuring Git archive][git-archive-docs]. +!!! warning "Automatic File Inclusion Behavior" + + **Important:** Simply installing `setuptools-scm` as a build dependency will automatically enable its file finder, which includes **all SCM-tracked files** in your source distributions. This happens even if you're not using setuptools-scm for versioning. + + - ✅ **Expected**: All Git/Mercurial tracked files will be included in your sdist + - ⚠️ **Surprise**: This includes development files, configs, tests, docs, etc. + - 🛠️ **Control**: Use `MANIFEST.in` to exclude unwanted files + + See the [File Finder Documentation](usage.md#file-finders-hook-makes-most-of-manifestin-unnecessary) for details. + [git-archive-docs]: usage.md#builtin-mechanisms-for-obtaining-version-numbers ## Basic usage diff --git a/docs/usage.md b/docs/usage.md index 49716ee6..8561e023 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -285,6 +285,12 @@ be kept in version control. It's strongly recommended to be put into gitignore. ### File finders hook makes most of `MANIFEST.in` unnecessary +!!! warning "Automatic File Inclusion" + + **`setuptools-scm` automatically provides a setuptools file finder by default.** This means that when you install setuptools-scm, it will automatically include **all SCM-tracked files** in your source distributions (sdist) without requiring a `MANIFEST.in` file. + + This automatic behavior can be surprising if you're not expecting it. The file finder is active as soon as setuptools-scm is installed in your build environment. + `setuptools-scm` implements a [file_finders] entry point which returns all files tracked by your SCM. This eliminates the need for a manually constructed `MANIFEST.in` in most cases where this @@ -293,6 +299,63 @@ would be required when not using `setuptools-scm`, namely: * To ensure all relevant files are packaged when running the `sdist` command. * When using [include_package_data] to include package data as part of the `build` or `bdist_wheel`. +#### How it works + +The file finder integration works through setuptools' plugin system: + +1. **Entry Point Registration**: setuptools-scm registers itself as a file finder via the `setuptools.file_finders` entry point +2. **Automatic Discovery**: When setuptools builds a source distribution, it automatically calls setuptools-scm to get the list of files +3. **SCM Integration**: setuptools-scm queries your SCM (Git, Mercurial) to get all tracked files +4. **File Inclusion**: All SCM-tracked files are automatically included in the sdist + +#### Controlling file inclusion + +**Using MANIFEST.in**: You can still use `MANIFEST.in` to override the automatic behavior: + +- **Exclude files**: Use `global-exclude` or `exclude` to remove files that are SCM-tracked but shouldn't be in the package +- **Include additional files**: Use `include` to add files that aren't SCM-tracked + +```text title="MANIFEST.in" +# Exclude development files +exclude *.nix +exclude .pre-commit-config.yaml +global-exclude *.pyc + +# Include additional files not in SCM +include data/special-file.dat +``` + +**Example of what gets included automatically**: + +- All files tracked by Git/Mercurial in your repository +- Includes source code, data files, documentation, etc. +- Excludes untracked files and files ignored by your SCM + +#### Troubleshooting + +**Too many files in your sdist?** + +1. Check what's being included: `python -m setuptools_scm ls` +2. Use `MANIFEST.in` to exclude unwanted files: + ```text + exclude development-file.txt + global-exclude *.log + prune unnecessary-directory/ + ``` + +**Files missing from your sdist?** + +1. Ensure files are tracked by your SCM: `git add` or `hg add` +2. For non-SCM files, add them via `MANIFEST.in`: + ```text + include important-file.txt + recursive-include data *.json + ``` + +**Disable automatic file finding** (not recommended): + +If you need to completely disable setuptools-scm's file finder (not recommended), you would need to uninstall setuptools-scm from your build environment and handle versioning differently. + `MANIFEST.in` may still be used: anything defined there overrides the hook. This is mostly useful to exclude files tracked in your SCM from packages, although in principle it can be used to explicitly include non-tracked files too. From 64dc8cd782b22e206f44a006c4a7fd1a63fdac22 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 21:15:17 +0200 Subject: [PATCH 027/162] Use file modification times for dirty working directory timestamps When setuptools-scm encounters a dirty working directory, it now uses the latest modification time of changed files instead of falling back to the current timestamp. This provides more meaningful version timestamps during local development. Changes: - Added get_dirty_tag_date() method to Git and Mercurial working directory classes - Enhanced timestamp logic in parsing functions to prioritize file mtimes - Updated documentation to explain new timestamp behavior - Maintains backward compatibility with clean repository behavior The logic flow is now: 1. Try to get node_date from HEAD commit 2. If that fails AND working directory is dirty, use latest file mtime 3. Only fall back to datetime.now() as last resort --- docs/usage.md | 91 +++++++++++++++++------------------- src/setuptools_scm/git.py | 54 ++++++++++++++++++++- src/setuptools_scm/hg.py | 61 +++++++++++++++++++++++- src/setuptools_scm/hg_git.py | 52 +++++++++++++++++++++ 4 files changed, 209 insertions(+), 49 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 8561e023..ed3b0f8d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -294,71 +294,68 @@ be kept in version control. It's strongly recommended to be put into gitignore. `setuptools-scm` implements a [file_finders] entry point which returns all files tracked by your SCM. This eliminates the need for a manually constructed `MANIFEST.in` in most cases where this -would be required when not using `setuptools-scm`, namely: +would be required when not using `setuptools-scm`. -* To ensure all relevant files are packaged when running the `sdist` command. - * When using [include_package_data] to include package data as part of the `build` or `bdist_wheel`. +[file_finders]: https://setuptools.pypa.io/en/stable/userguide/extension.html #### How it works -The file finder integration works through setuptools' plugin system: - -1. **Entry Point Registration**: setuptools-scm registers itself as a file finder via the `setuptools.file_finders` entry point -2. **Automatic Discovery**: When setuptools builds a source distribution, it automatically calls setuptools-scm to get the list of files -3. **SCM Integration**: setuptools-scm queries your SCM (Git, Mercurial) to get all tracked files -4. **File Inclusion**: All SCM-tracked files are automatically included in the sdist +1. **Automatic Discovery**: When building source distributions (`python -m build --sdist`), setuptools automatically calls the `setuptools-scm` file finder +2. **SCM Integration**: The file finder queries your SCM (Git/Mercurial) for all tracked files +3. **Inclusion**: All tracked files are automatically included in the sdist #### Controlling file inclusion -**Using MANIFEST.in**: You can still use `MANIFEST.in` to override the automatic behavior: +**To exclude unwanted files:** -- **Exclude files**: Use `global-exclude` or `exclude` to remove files that are SCM-tracked but shouldn't be in the package -- **Include additional files**: Use `include` to add files that aren't SCM-tracked +1. **Use `MANIFEST.in`** to exclude specific files/patterns: + ``` + exclude development.txt + recursive-exclude tests *.pyc + ``` -```text title="MANIFEST.in" -# Exclude development files -exclude *.nix -exclude .pre-commit-config.yaml -global-exclude *.pyc +2. **Configure Git archive** (for Git repositories): + ```bash + # Add to .gitattributes + tests/ export-ignore + *.md export-ignore + ``` -# Include additional files not in SCM -include data/special-file.dat -``` +3. **Use `.hgignore`** or **Mercurial archive configuration** (for Mercurial repositories) -**Example of what gets included automatically**: +#### Troubleshooting -- All files tracked by Git/Mercurial in your repository -- Includes source code, data files, documentation, etc. -- Excludes untracked files and files ignored by your SCM +**Problem: Unwanted files in my package** +- ✅ **Solution**: Add exclusions to `MANIFEST.in` +- ✅ **Alternative**: Use Git/Mercurial archive configuration -#### Troubleshooting +**Problem: Missing files in package** +- ✅ **Check**: Are the files tracked in your SCM? +- ✅ **Solution**: `git add` missing files or override with `MANIFEST.in` -**Too many files in your sdist?** +**Problem: File finder not working** +- ✅ **Check**: Is setuptools-scm installed in your build environment? +- ✅ **Check**: Are you in a valid SCM repository? -1. Check what's being included: `python -m setuptools_scm ls` -2. Use `MANIFEST.in` to exclude unwanted files: - ```text - exclude development-file.txt - global-exclude *.log - prune unnecessary-directory/ - ``` +### Timestamps for Local Development Versions -**Files missing from your sdist?** +!!! info "Improved Timestamp Behavior" -1. Ensure files are tracked by your SCM: `git add` or `hg add` -2. For non-SCM files, add them via `MANIFEST.in`: - ```text - include important-file.txt - recursive-include data *.json - ``` + When your working directory has uncommitted changes (dirty), setuptools-scm now uses the **actual modification time of changed files** instead of the current time for local version schemes like `node-and-date`. + + **Before**: Dirty working directories always used current time (`now`) + **Now**: Uses the latest modification time of changed files, falling back to current time only if no changed files are found + + This provides more stable and meaningful timestamps that reflect when you actually made changes to your code. -**Disable automatic file finding** (not recommended): +**How it works:** -If you need to completely disable setuptools-scm's file finder (not recommended), you would need to uninstall setuptools-scm from your build environment and handle versioning differently. +1. **Clean repository**: Uses commit timestamp from SCM +2. **Dirty repository**: Uses latest modification time of changed files +3. **Fallback**: Uses current time if no modification times can be determined -`MANIFEST.in` may still be used: anything defined there overrides the hook. -This is mostly useful to exclude files tracked in your SCM from packages, -although in principle it can be used to explicitly include non-tracked files too. +**Benefits:** -[file_finders]: https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-support-for-revision-control-systems -[include_package_data]: https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files +- More stable builds during development +- Timestamps reflect actual change times +- Better for reproducible development workflows diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 4773f49c..49dcad31 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -144,6 +144,45 @@ def parse_timestamp(timestamp_text: str) -> date | None: error_msg="logging the iso date for head failed", ) + def get_dirty_tag_date(self) -> date | None: + """Get the latest modification time of changed files in the working directory. + + Returns the date of the most recently modified file that has changes, + or None if no files are changed or if an error occurs. + """ + if not self.is_dirty(): + return None + + try: + # Get list of changed files + changed_files_res = run_git(["diff", "--name-only"], self.path) + if changed_files_res.returncode != 0: + return None + + changed_files = changed_files_res.stdout.strip().split("\n") + if not changed_files or changed_files == [""]: + return None + + latest_mtime = 0.0 + for filepath in changed_files: + full_path = self.path / filepath + try: + file_stat = full_path.stat() + latest_mtime = max(latest_mtime, file_stat.st_mtime) + except OSError: + # File might not exist or be accessible, skip it + continue + + if latest_mtime > 0: + # Convert to UTC date + dt = datetime.fromtimestamp(latest_mtime, timezone.utc) + return dt.date() + + except Exception as e: + log.debug("Failed to get dirty tag date: %s", e) + + return None + def is_shallow(self) -> bool: return self.path.joinpath(".git/shallow").is_file() @@ -277,7 +316,20 @@ def _git_parse_inner( tag=tag, distance=distance, dirty=dirty, node=node, config=config ) branch = wd.get_branch() - node_date = wd.get_head_date() or datetime.now(timezone.utc).date() + node_date = wd.get_head_date() + + # If we can't get node_date from HEAD (e.g., no commits yet), + # and the working directory is dirty, try to use the latest + # modification time of changed files instead of current time + if node_date is None and wd.is_dirty(): + dirty_date = wd.get_dirty_tag_date() + if dirty_date is not None: + node_date = dirty_date + + # Final fallback to current time + if node_date is None: + node_date = datetime.now(timezone.utc).date() + return dataclasses.replace(version, branch=branch, node_date=node_date) diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index f1c1560d..59286111 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -52,7 +52,17 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: check=True, ).stdout.split("\n") dirty = bool(int(dirty_str)) - node_date = datetime.date.fromisoformat(dirty_date if dirty else node_date_str) + + # For dirty working directories, try to use the latest file modification time + # before falling back to the hg id date + if dirty: + file_mod_date = self.get_dirty_tag_date() + if file_mod_date is not None: + node_date = file_mod_date + else: + node_date = datetime.date.fromisoformat(dirty_date) + else: + node_date = datetime.date.fromisoformat(node_date_str) if node == "0" * len(node): log.debug("initial node %s", self.path) @@ -144,6 +154,55 @@ def check_changes_since_tag(self, tag: str | None) -> bool: return bool(self.hg_log(revset, ".")) + def get_dirty_tag_date(self) -> datetime.date | None: + """Get the latest modification time of changed files in the working directory. + + Returns the date of the most recently modified file that has changes, + or None if no files are changed or if an error occurs. + """ + try: + # Check if working directory is dirty first + res = _run([HG_COMMAND, "id", "-T", "{dirty}"], cwd=self.path) + if res.returncode != 0 or not bool(res.stdout): + return None + + # Get list of changed files using hg status + status_res = _run([HG_COMMAND, "status", "-m", "-a", "-r"], cwd=self.path) + if status_res.returncode != 0: + return None + + changed_files = [] + for line in status_res.stdout.strip().split("\n"): + if line and len(line) > 2: + # Format is "M filename" or "A filename" etc. + filepath = line[2:] # Skip status char and space + changed_files.append(filepath) + + if not changed_files: + return None + + latest_mtime = 0.0 + for filepath in changed_files: + full_path = self.path / filepath + try: + file_stat = full_path.stat() + latest_mtime = max(latest_mtime, file_stat.st_mtime) + except OSError: + # File might not exist or be accessible, skip it + continue + + if latest_mtime > 0: + # Convert to UTC date + dt = datetime.datetime.fromtimestamp( + latest_mtime, datetime.timezone.utc + ) + return dt.date() + + except Exception as e: + log.debug("Failed to get dirty tag date: %s", e) + + return None + def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: _require_command(HG_COMMAND) diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index f2336e3c..92163242 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -47,6 +47,58 @@ def get_head_date(self) -> date | None: [HG_COMMAND, "log", "-r", ".", "-T", "{shortdate(date)}"], cwd=self.path ).parse_success(parse=date.fromisoformat, error_msg="head date err") + def get_dirty_tag_date(self) -> date | None: + """Get the latest modification time of changed files in the working directory. + + Returns the date of the most recently modified file that has changes, + or None if no files are changed or if an error occurs. + """ + if not self.is_dirty(): + return None + + try: + from datetime import datetime + from datetime import timezone + + # Get list of changed files using hg status + status_res = _run([HG_COMMAND, "status", "-m", "-a", "-r"], cwd=self.path) + if status_res.returncode != 0: + return None + + changed_files = [] + for line in status_res.stdout.strip().split("\n"): + if line and len(line) > 2: + # Format is "M filename" or "A filename" etc. + filepath = line[2:] # Skip status char and space + changed_files.append(filepath) + + if not changed_files: + return None + + latest_mtime = 0.0 + for filepath in changed_files: + full_path = self.path / filepath + try: + file_stat = full_path.stat() + latest_mtime = max(latest_mtime, file_stat.st_mtime) + except OSError: + # File might not exist or be accessible, skip it + continue + + if latest_mtime > 0: + # Convert to UTC date + dt = datetime.fromtimestamp(latest_mtime, timezone.utc) + return dt.date() + + except Exception as e: + # Use the parent's log module + import logging + + log = logging.getLogger(__name__) + log.debug("Failed to get dirty tag date: %s", e) + + return None + def is_shallow(self) -> bool: return False From fd05c8ad72813f5fdee816d48d38f280daa4086e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 21:20:05 +0200 Subject: [PATCH 028/162] Refactor: Extract file mtime finding into shared helper function Create get_latest_file_mtime() helper function in scm_workdir.py to eliminate code duplication across Git, Mercurial, and hybrid implementations. Changes: - Added get_latest_file_mtime() function to scm_workdir.py - Updated GitWorkdir.get_dirty_tag_date() to use shared helper - Updated HgWorkdir.get_dirty_tag_date() to use shared helper - Updated GitWorkdirHgClient.get_dirty_tag_date() to use shared helper - Removed duplicated mtime calculation logic from all implementations - Cleaned up unused imports (datetime/timezone in hg_git.py) This improves maintainability and ensures consistent behavior across all VCS implementations. --- src/setuptools_scm/git.py | 22 +++-------------- src/setuptools_scm/hg.py | 21 ++--------------- src/setuptools_scm/hg_git.py | 26 ++------------------- src/setuptools_scm/scm_workdir.py | 39 +++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 62 deletions(-) diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 49dcad31..150629b1 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -26,6 +26,7 @@ from ._run_cmd import run as _run from .integration import data_from_mime from .scm_workdir import Workdir +from .scm_workdir import get_latest_file_mtime from .version import ScmVersion from .version import meta from .version import tag_to_version @@ -160,28 +161,11 @@ def get_dirty_tag_date(self) -> date | None: return None changed_files = changed_files_res.stdout.strip().split("\n") - if not changed_files or changed_files == [""]: - return None - - latest_mtime = 0.0 - for filepath in changed_files: - full_path = self.path / filepath - try: - file_stat = full_path.stat() - latest_mtime = max(latest_mtime, file_stat.st_mtime) - except OSError: - # File might not exist or be accessible, skip it - continue - - if latest_mtime > 0: - # Convert to UTC date - dt = datetime.fromtimestamp(latest_mtime, timezone.utc) - return dt.date() + return get_latest_file_mtime(changed_files, self.path) except Exception as e: log.debug("Failed to get dirty tag date: %s", e) - - return None + return None def is_shallow(self) -> bool: return self.path.joinpath(".git/shallow").is_file() diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index 59286111..d41dae1d 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -11,6 +11,7 @@ from ._version_cls import Version from .integration import data_from_mime from .scm_workdir import Workdir +from .scm_workdir import get_latest_file_mtime from .version import ScmVersion from .version import meta from .version import tag_to_version @@ -178,25 +179,7 @@ def get_dirty_tag_date(self) -> datetime.date | None: filepath = line[2:] # Skip status char and space changed_files.append(filepath) - if not changed_files: - return None - - latest_mtime = 0.0 - for filepath in changed_files: - full_path = self.path / filepath - try: - file_stat = full_path.stat() - latest_mtime = max(latest_mtime, file_stat.st_mtime) - except OSError: - # File might not exist or be accessible, skip it - continue - - if latest_mtime > 0: - # Convert to UTC date - dt = datetime.datetime.fromtimestamp( - latest_mtime, datetime.timezone.utc - ) - return dt.date() + return get_latest_file_mtime(changed_files, self.path) except Exception as e: log.debug("Failed to get dirty tag date: %s", e) diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index 92163242..f040eb9e 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -13,6 +13,7 @@ from .git import GitWorkdir from .hg import HG_COMMAND from .hg import HgWorkdir +from .scm_workdir import get_latest_file_mtime log = logging.getLogger(__name__) @@ -57,9 +58,6 @@ def get_dirty_tag_date(self) -> date | None: return None try: - from datetime import datetime - from datetime import timezone - # Get list of changed files using hg status status_res = _run([HG_COMMAND, "status", "-m", "-a", "-r"], cwd=self.path) if status_res.returncode != 0: @@ -72,29 +70,9 @@ def get_dirty_tag_date(self) -> date | None: filepath = line[2:] # Skip status char and space changed_files.append(filepath) - if not changed_files: - return None - - latest_mtime = 0.0 - for filepath in changed_files: - full_path = self.path / filepath - try: - file_stat = full_path.stat() - latest_mtime = max(latest_mtime, file_stat.st_mtime) - except OSError: - # File might not exist or be accessible, skip it - continue - - if latest_mtime > 0: - # Convert to UTC date - dt = datetime.fromtimestamp(latest_mtime, timezone.utc) - return dt.date() + return get_latest_file_mtime(changed_files, self.path) except Exception as e: - # Use the parent's log module - import logging - - log = logging.getLogger(__name__) log.debug("Failed to get dirty tag date: %s", e) return None diff --git a/src/setuptools_scm/scm_workdir.py b/src/setuptools_scm/scm_workdir.py index 9879549d..b3ca7aa8 100644 --- a/src/setuptools_scm/scm_workdir.py +++ b/src/setuptools_scm/scm_workdir.py @@ -1,11 +1,50 @@ from __future__ import annotations +import logging + from dataclasses import dataclass +from datetime import date +from datetime import datetime +from datetime import timezone from pathlib import Path from ._config import Configuration from .version import ScmVersion +log = logging.getLogger(__name__) + + +def get_latest_file_mtime(changed_files: list[str], base_path: Path) -> date | None: + """Get the latest modification time of the given files. + + Args: + changed_files: List of relative file paths + base_path: Base directory path to resolve relative paths + + Returns: + The date of the most recently modified file, or None if no valid files found + """ + if not changed_files or changed_files == [""]: + return None + + latest_mtime = 0.0 + for filepath in changed_files: + full_path = base_path / filepath + try: + file_stat = full_path.stat() + latest_mtime = max(latest_mtime, file_stat.st_mtime) + except OSError: + # File might not exist or be accessible, skip it + log.debug("Failed to get mtime for %s", full_path) + continue + + if latest_mtime > 0: + # Convert to UTC date + dt = datetime.fromtimestamp(latest_mtime, timezone.utc) + return dt.date() + + return None + @dataclass() class Workdir: From 2809c454c9595eef78f7facf7d755dde3b13413b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 21:29:39 +0200 Subject: [PATCH 029/162] Refactor: Reduce complexity of HgWorkdir.get_meta method Break down the complex get_meta method into smaller, focused helper methods to improve readability and reduce cyclomatic complexity below the linting threshold. Changes: - Extract _get_node_info() for getting node/tags/date from hg log - Extract _get_branch_info() for getting branch/dirty status - Extract _get_node_date() for determining appropriate node date - Extract _is_initial_node() for checking empty repository state - Extract _create_initial_meta() for creating initial repository metadata - Extract _parse_tags() for filtering and processing tags - Extract _get_version_from_tags() for converting tags to versions - Extract _get_distance_based_version() for distance-based versioning - Fix type safety by properly handling None from tag_to_version Each helper method has a single responsibility and clear documentation. The main get_meta method now has a clear, linear flow that's easy to follow. Complexity reduced from 11 to under the 10 threshold. --- pyproject.toml | 3 +- src/setuptools_scm/hg.py | 120 ++- uv.lock | 1910 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 2002 insertions(+), 31 deletions(-) create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index ffbd9778..7dc6210b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dependencies = [ ] [project.optional-dependencies] docs = [ - "entangled-cli~=2.0", + #"entangled-cli~=2.0", "mkdocs", "mkdocs-entangled-plugin", "mkdocs-include-markdown-plugin", @@ -67,6 +67,7 @@ test = [ "rich", 'typing-extensions; python_version < "3.11"', "wheel", + "mercurial" ] toml = [ ] diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index d41dae1d..238da909 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -36,57 +36,113 @@ def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None: return cls(Path(res.stdout)) def get_meta(self, config: Configuration) -> ScmVersion | None: - node: str - tags_str: str - node_date_str: str - node, tags_str, node_date_str = self.hg_log( - ".", "{node}\n{tag}\n{date|shortdate}" - ).split("\n") - # TODO: support bookmarks and topics (but nowadays bookmarks are # mainly used to emulate Git branches, which is already supported with # the dedicated class GitWorkdirHgClient) + node_info = self._get_node_info() + if node_info is None: + return None + + node, tags_str, node_date_str = node_info + branch_info = self._get_branch_info() + branch, dirty, dirty_date = branch_info + + # Determine the appropriate node date + node_date = self._get_node_date(dirty, node_date_str, dirty_date) + + # Handle initial/empty repository + if self._is_initial_node(node): + return self._create_initial_meta(config, dirty, branch, node_date) + + node = "h" + node[:7] + tags = self._parse_tags(tags_str) + + # Try to get version from current tags + tag_version = self._get_version_from_tags(tags, config) + if tag_version: + return meta(tag_version, dirty=dirty, branch=branch, config=config) + + # Fall back to distance-based versioning + return self._get_distance_based_version(config, dirty, branch, node, node_date) + + def _get_node_info(self) -> tuple[str, str, str] | None: + """Get node, tags, and date information from mercurial log.""" + try: + node, tags_str, node_date_str = self.hg_log( + ".", "{node}\n{tag}\n{date|shortdate}" + ).split("\n") + return node, tags_str, node_date_str + except ValueError: + log.exception("Failed to get node info") + return None + + def _get_branch_info(self) -> tuple[str, bool, str]: + """Get branch name, dirty status, and dirty date.""" branch, dirty_str, dirty_date = _run( [HG_COMMAND, "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"], cwd=self.path, check=True, ).stdout.split("\n") dirty = bool(int(dirty_str)) + return branch, dirty, dirty_date - # For dirty working directories, try to use the latest file modification time - # before falling back to the hg id date + def _get_node_date( + self, dirty: bool, node_date_str: str, dirty_date: str + ) -> datetime.date: + """Get the appropriate node date, preferring file modification times for dirty repos.""" if dirty: file_mod_date = self.get_dirty_tag_date() if file_mod_date is not None: - node_date = file_mod_date - else: - node_date = datetime.date.fromisoformat(dirty_date) + return file_mod_date + # Fall back to hg id date for dirty repos + return datetime.date.fromisoformat(dirty_date) else: - node_date = datetime.date.fromisoformat(node_date_str) - - if node == "0" * len(node): - log.debug("initial node %s", self.path) - return meta( - Version("0.0"), - config=config, - dirty=dirty, - branch=branch, - node_date=node_date, - ) + return datetime.date.fromisoformat(node_date_str) - node = "h" + node[:7] + def _is_initial_node(self, node: str) -> bool: + """Check if this is an initial/empty repository node.""" + return node == "0" * len(node) + + def _create_initial_meta( + self, config: Configuration, dirty: bool, branch: str, node_date: datetime.date + ) -> ScmVersion: + """Create metadata for initial/empty repository.""" + log.debug("initial node %s", self.path) + return meta( + Version("0.0"), + config=config, + dirty=dirty, + branch=branch, + node_date=node_date, + ) + def _parse_tags(self, tags_str: str) -> list[str]: + """Parse and filter tags from mercurial output.""" tags = tags_str.split() if "tip" in tags: # tip is not a real tag tags.remove("tip") + return tags + def _get_version_from_tags( + self, tags: list[str], config: Configuration + ) -> Version | None: + """Try to get a version from the current tags.""" if tags: tag = tag_to_version(tags[0], config) - if tag: - return meta(tag, dirty=dirty, branch=branch, config=config) + return tag + return None + def _get_distance_based_version( + self, + config: Configuration, + dirty: bool, + branch: str, + node: str, + node_date: datetime.date, + ) -> ScmVersion | None: + """Get version based on distance from latest tag.""" try: tag_str = self.get_latest_normalizable_tag() if tag_str is None: @@ -98,8 +154,13 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: tag = Version("0.0") dist += 1 else: - tag = tag_to_version(tag_str, config=config) - assert tag is not None + maybe_tag = tag_to_version(tag_str, config=config) + if maybe_tag is None: + # If tag conversion fails, treat as no tag found + tag = Version("0.0") + dist += 1 + else: + tag = maybe_tag if self.check_changes_since_tag(tag_str) or dirty: return meta( @@ -117,8 +178,7 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: except ValueError: # unpacking failed, old hg log.exception("error") - - return None + return None def hg_log(self, revset: str, template: str) -> str: cmd = [HG_COMMAND, "log", "-r", revset, "-T", template] diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..e51dd5d8 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1910 @@ +version = 1 +revision = 2 +requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "argh" +version = "0.30.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/12/353fc6b87bfaf98fe5a0c95e8cba03acd24ab0eea99d1307e1ab7a3809ba/argh-0.30.5.tar.gz", hash = "sha256:b37dfd617a09d19a4a7bcaed0e060b288bc7ac8dfdc0facf886a49a25ff33728", size = 63041, upload-time = "2023-12-25T22:05:32.15Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/51/99d9dfcb588e15b4d9630f98f84d4e766d03b47da52839395057dbbe2df4/argh-0.30.5-py3-none-any.whl", hash = "sha256:3844e955d160f0689a3cdca06a59dfcfbf1fcea70029d67d473f73503341e0d8", size = 44635, upload-time = "2023-12-25T22:05:29.35Z" }, +] + +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "python_full_version < '3.9'" }, + { name = "wheel", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "5.7.post1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/df/30/903f35159c87ff1d92aa3fcf8cb52de97632a21e0ae43ed940f5d033e01a/backrefs-5.7.post1.tar.gz", hash = "sha256:8b0f83b770332ee2f1c8244f4e03c77d127a0fa529328e6a0e77fa25bee99678", size = 6582270, upload-time = "2024-06-16T18:38:20.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/bb/47fc255d1060dcfd55b460236380edd8ebfc5b2a42a0799ca90c9fc983e3/backrefs-5.7.post1-py310-none-any.whl", hash = "sha256:c5e3fd8fd185607a7cb1fefe878cfb09c34c0be3c18328f12c574245f1c0287e", size = 380429, upload-time = "2024-06-16T18:38:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/39ef491caef3abae945f5a5fd72830d3b596bfac0630508629283585e213/backrefs-5.7.post1-py311-none-any.whl", hash = "sha256:712ea7e494c5bf3291156e28954dd96d04dc44681d0e5c030adf2623d5606d51", size = 392234, upload-time = "2024-06-16T18:38:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/6a/00/33403f581b732ca70fdebab558e8bbb426a29c34e0c3ed674a479b74beea/backrefs-5.7.post1-py312-none-any.whl", hash = "sha256:a6142201c8293e75bce7577ac29e1a9438c12e730d73a59efdd1b75528d1a6c5", size = 398110, upload-time = "2024-06-16T18:38:14.257Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ea/df0ac74a26838f6588aa012d5d801831448b87d0a7d0aefbbfabbe894870/backrefs-5.7.post1-py38-none-any.whl", hash = "sha256:ec61b1ee0a4bfa24267f6b67d0f8c5ffdc8e0d7dc2f18a2685fd1d8d9187054a", size = 369477, upload-time = "2024-06-16T18:38:16.196Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e8/e43f535c0a17a695e5768670fc855a0e5d52dc0d4135b3915bfa355f65ac/backrefs-5.7.post1-py39-none-any.whl", hash = "sha256:05c04af2bf752bb9a6c9dcebb2aff2fab372d3d9d311f2a138540e307756bd3a", size = 380429, upload-time = "2024-06-16T18:38:18.079Z" }, +] + +[[package]] +name = "backrefs" +version = "5.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, +] + +[[package]] +name = "bracex" +version = "2.5.post1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641, upload-time = "2024-09-28T21:41:22.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/02/8db98cdc1a58e0abd6716d5e63244658e6e63513c65f469f34b6f1053fd0/bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6", size = 11558, upload-time = "2024-09-28T21:41:21.016Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "brei" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argh", marker = "python_full_version >= '3.11'" }, + { name = "rich", marker = "python_full_version >= '3.11'" }, + { name = "rich-argparse", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/97/503b9bc095501678cd6e10a940e042d36f2a77ee41b798e93477d142543d/brei-0.2.4.tar.gz", hash = "sha256:fb8a1f191a1c70a81a9df366c530e01938b8082e7198e898d4ea8eb53066fa92", size = 18853, upload-time = "2024-11-25T10:17:11.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/6d/e5c216146c0654cbf7d4e0567a075e3f13f2700bc8812b9ec3a35bf1099b/brei-0.2.4-py3-none-any.whl", hash = "sha256:46967640f1aebe3c698456434a8ff0b2901860c4544778ab763803875e925155", size = 22436, upload-time = "2024-11-25T10:17:10.292Z" }, +] + +[[package]] +name = "build" +version = "1.2.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, +] + +[[package]] +name = "certifi" +version = "2025.7.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fd/f700cfd4ad876def96d2c769d8a32d808b12d1010b6003dc6639157f99ee/charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", size = 198257, upload-time = "2025-05-02T08:33:45.511Z" }, + { url = "https://files.pythonhosted.org/packages/3a/95/6eec4cbbbd119e6a402e3bfd16246785cc52ce64cf21af2ecdf7b3a08e91/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", size = 143453, upload-time = "2025-05-02T08:33:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/d4f913660383b3d93dbe6f687a312ea9f7e89879ae883c4e8942048174d4/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", size = 153130, upload-time = "2025-05-02T08:33:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/7540141529eabc55bf19cc05cd9b61c2078bebfcdbd3e799af99b777fc28/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", size = 145688, upload-time = "2025-05-02T08:33:52.828Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bb/d76d3d6e340fb0967c43c564101e28a78c9a363ea62f736a68af59ee3683/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", size = 147418, upload-time = "2025-05-02T08:33:54.718Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ef/b7c1f39c0dc3808160c8b72e0209c2479393966313bfebc833533cfff9cc/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", size = 150066, upload-time = "2025-05-02T08:33:56.597Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/4e47cc23d2a4a5eb6ed7d6f0f8cda87d753e2f8abc936d5cf5ad2aae8518/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", size = 144499, upload-time = "2025-05-02T08:33:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9c/efdf59dd46593cecad0548d36a702683a0bdc056793398a9cd1e1546ad21/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", size = 152954, upload-time = "2025-05-02T08:34:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/b3/4e8b73f7299d9aaabd7cd26db4a765f741b8e57df97b034bb8de15609002/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", size = 155876, upload-time = "2025-05-02T08:34:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/53/cb/6fa0ccf941a069adce3edb8a1e430bc80e4929f4d43b5140fdf8628bdf7d/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", size = 153186, upload-time = "2025-05-02T08:34:04.481Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c6/80b93fabc626b75b1665ffe405e28c3cef0aae9237c5c05f15955af4edd8/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", size = 148007, upload-time = "2025-05-02T08:34:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/41/eb/c7367ac326a2628e4f05b5c737c86fe4a8eb3ecc597a4243fc65720b3eeb/charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", size = 97923, upload-time = "2025-05-02T08:34:08.792Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/1c82646582ccf2c757fa6af69b1a3ea88744b8d2b4ab93b7686b2533e023/charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", size = 105020, upload-time = "2025-05-02T08:34:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "copier" +version = "9.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11'" }, + { name = "dunamai", marker = "python_full_version >= '3.11'" }, + { name = "funcy", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "jinja2-ansible-filters", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pathspec", marker = "python_full_version >= '3.11'" }, + { name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "plumbum", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "questionary", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/78/1246b4a1ddeb404037f76efedab9b42ada4e6ba56c574d8846e2a6b8f498/copier-9.8.0.tar.gz", hash = "sha256:343ac1eb65e678aa355690d7f19869ef07cabf837f511a87ed452443c085ec58", size = 579353, upload-time = "2025-07-07T18:47:03.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/ed/839c91ff365f24756c90189e07f9de226d2e37cbc03c635f5d16d45d79cb/copier-9.8.0-py3-none-any.whl", hash = "sha256:ca0bee47f198b66cec926c4f1a3aa77f11ee0102624369c10e42ca9058c0a891", size = 55744, upload-time = "2025-07-07T18:47:01.905Z" }, +] + +[[package]] +name = "dunamai" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/2f/194d9a34c4d831c6563d2d990720850f0baef9ab60cb4ad8ae0eff6acd34/dunamai-1.25.0.tar.gz", hash = "sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1", size = 46155, upload-time = "2025-07-04T19:25:56.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/41/04e2a649058b0713b00d6c9bd22da35618bb157289e05d068e51fddf8d7e/dunamai-1.25.0-py3-none-any.whl", hash = "sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab", size = 27022, upload-time = "2025-07-04T19:25:54.863Z" }, +] + +[[package]] +name = "entangled-cli" +version = "2.1.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argh", marker = "python_full_version >= '3.11'" }, + { name = "brei", marker = "python_full_version >= '3.11'" }, + { name = "copier", marker = "python_full_version >= '3.11'" }, + { name = "filelock", marker = "python_full_version >= '3.11'" }, + { name = "mawk", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "rich", marker = "python_full_version >= '3.11'" }, + { name = "rich-argparse", marker = "python_full_version >= '3.11'" }, + { name = "tomlkit", marker = "python_full_version >= '3.11'" }, + { name = "watchdog", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/35/568227a7e821e42520f769ca9bee162dfd8ef03e18616d1201577a2e25e0/entangled_cli-2.1.13.tar.gz", hash = "sha256:c17b1e479f1ac9689187a4ed7eb31c066af3458cdc663afffcb0f10051228642", size = 36900, upload-time = "2025-04-24T23:01:11.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/8c/cf9c69a44aa28900ceef8ab17aafc2beefab13d3cab6b771a4e3109e3c2e/entangled_cli-2.1.13-py3-none-any.whl", hash = "sha256:01822e05e393934ec1bc9f867d179581703282352b836b69d5cf654ee8e6e6b7", size = 50175, upload-time = "2025-04-24T23:01:09.981Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "funcy" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/b8/c6081521ff70afdff55cd9512b2220bbf4fa88804dae51d1b57b4b58ef32/funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb", size = 537931, upload-time = "2023-03-28T06:22:46.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0", size = 30891, upload-time = "2023-03-28T06:22:42.576Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "astunparse", marker = "python_full_version < '3.9'" }, + { name = "colorama", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e9/b2c86ad9d69053e497a24ceb25d661094fb321ab4ed39a8b71793dcbae82/griffe-1.4.0.tar.gz", hash = "sha256:8fccc585896d13f1221035d32c50dec65830c87d23f9adb9b1e6f3d63574f7f5", size = 381028, upload-time = "2024-10-11T12:53:54.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/7c/e9e66869c2e4c9b378474e49c993128ec0131ef4721038b6d06e50538caf/griffe-1.4.0-py3-none-any.whl", hash = "sha256:e589de8b8c137e99a46ec45f9598fc0ac5b6868ce824b24db09c02d117b89bc5", size = 127015, upload-time = "2024-10-11T12:53:52.383Z" }, +] + +[[package]] +name = "griffe" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/72/10c5799440ce6f3001b7913988b50a99d7b156da71fe19be06178d5a2dd5/griffe-1.8.0.tar.gz", hash = "sha256:0b4658443858465c13b2de07ff5e15a1032bc889cfafad738a476b8b97bb28d7", size = 401098, upload-time = "2025-07-22T23:45:54.629Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/c4/a839fcc28bebfa72925d9121c4d39398f77f95bcba0cf26c972a0cfb1de7/griffe-1.8.0-py3-none-any.whl", hash = "sha256:110faa744b2c5c84dd432f4fa9aa3b14805dd9519777dd55e8db214320593b02", size = 132487, upload-time = "2025-07-22T23:45:52.778Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "zipp", version = "3.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jinja2-ansible-filters" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/27/fa186af4b246eb869ffca8ffa42d92b05abaec08c99329e74d88b2c46ec7/jinja2-ansible-filters-1.3.2.tar.gz", hash = "sha256:07c10cf44d7073f4f01102ca12d9a2dc31b41d47e4c61ed92ef6a6d2669b356b", size = 16945, upload-time = "2022-06-30T14:08:50.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34", size = 18975, upload-time = "2022-06-30T14:08:49.571Z" }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload-time = "2024-08-16T15:55:17.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" }, +] + +[[package]] +name = "markdown" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload-time = "2024-02-02T16:30:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload-time = "2024-02-02T16:30:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload-time = "2024-02-02T16:30:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload-time = "2024-02-02T16:31:01.582Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload-time = "2024-02-02T16:31:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload-time = "2024-02-02T16:31:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload-time = "2024-02-02T16:31:05.53Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload-time = "2024-02-02T16:31:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload-time = "2024-02-02T16:31:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload-time = "2024-02-02T16:31:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload-time = "2024-02-02T16:31:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload-time = "2024-02-02T16:31:11.442Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload-time = "2024-02-02T16:31:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload-time = "2024-02-02T16:31:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload-time = "2024-02-02T16:31:14.812Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload-time = "2024-02-02T16:31:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload-time = "2024-02-02T16:31:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload-time = "2024-02-02T16:31:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload-time = "2024-02-02T16:31:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload-time = "2024-02-02T16:31:20.96Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, +] + +[[package]] +name = "mawk" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/42/0cb0d6d02649f50e2a8918a350152d8d139839cf74befeeeb83668302e48/mawk-0.1.4.tar.gz", hash = "sha256:4e115b2f7eae97406bf2360bafba22efee03d29ff298436a69db506b1535d2f1", size = 7216, upload-time = "2023-06-04T12:53:42.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/29/b4ddddd1ae74ce2651e7eedf1871a6b2240433eeec070622f81b985d5e70/mawk-0.1.4-py3-none-any.whl", hash = "sha256:8ab7ce0808d10769f8aa05af8448046c290af5f529db874b44a8fd56056c4462", size = 7941, upload-time = "2023-06-04T12:53:41.112Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "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" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag", version = "0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyyaml-env-tag", version = "1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262, upload-time = "2024-09-01T18:29:18.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522, upload-time = "2024-09-01T18:29:16.605Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, +] + +[[package]] +name = "mkdocs-entangled-plugin" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +dependencies = [ + { name = "mkdocs", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/a2/f94a86faae15f76daf39735f4aed443a9fb5af5e568919f9d472cbc222b5/mkdocs_entangled_plugin-0.2.0.tar.gz", hash = "sha256:7f585cc5811fb097aad0435c20929108348b293830e4b8978b55e19f24631908", size = 8672, upload-time = "2023-05-03T23:56:43.771Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/c75092d147093ccf6994bacddd58b40d3fa92092d9ec236356f399a4d44a/mkdocs_entangled_plugin-0.2.0-py3-none-any.whl", hash = "sha256:f386631c11e6c19a41f89902f83275c274db8abacf5adac9cff88394ec9e1788", size = 10821, upload-time = "2023-05-03T23:56:42.589Z" }, +] + +[[package]] +name = "mkdocs-entangled-plugin" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "entangled-cli", marker = "python_full_version >= '3.11'" }, + { name = "mawk", marker = "python_full_version >= '3.11'" }, + { name = "mkdocs", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/1b/af0757f0a5a60a636903f520ca6164df314068374f765a072d06b65bad7a/mkdocs_entangled_plugin-0.4.0.tar.gz", hash = "sha256:e78f1ff3b55a3838f6a5d6a46d9280484af4520a4679f9bccd8b638406b026c8", size = 7527, upload-time = "2023-10-14T12:20:40.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/d0/9bf18d41e1e2eda91d67a3493778016001402781dcf9f3c391d94fb50545/mkdocs_entangled_plugin-0.4.0-py3-none-any.whl", hash = "sha256:af4fe84e2218e2c0a805d20161977e34cd56f8af24b4d1a88be4c258176b81bb", size = 9408, upload-time = "2023-10-14T12:20:18.937Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mergedeep" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "mkdocs", marker = "python_full_version < '3.9'" }, + { name = "wcmatch", version = "10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/fe/4bb438d0f58995f81e2616d640f7efe0df9b1f992cba706a9453676c9140/mkdocs_include_markdown_plugin-6.2.2.tar.gz", hash = "sha256:f2bd5026650492a581d2fd44be6c22f90391910d76582b96a34c264f2d17875d", size = 21045, upload-time = "2024-08-10T23:36:41.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/d9/7b2b09b4870a2cd5a80628c74553307205a8474aabe128b66e305b56ac30/mkdocs_include_markdown_plugin-6.2.2-py3-none-any.whl", hash = "sha256:d293950f6499d2944291ca7b9bc4a60e652bbfd3e3a42b564f6cceee268694e7", size = 24643, upload-time = "2024-08-10T23:36:39.736Z" }, +] + +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "7.1.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mkdocs", marker = "python_full_version >= '3.9'" }, + { name = "wcmatch", version = "10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/17/988d97ac6849b196f54d45ca9c60ca894880c160a512785f03834704b3d9/mkdocs_include_markdown_plugin-7.1.6.tar.gz", hash = "sha256:a0753cb82704c10a287f1e789fc9848f82b6beb8749814b24b03dd9f67816677", size = 23391, upload-time = "2025-06-13T18:25:51.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/a1/6cf1667a05e5f468e1263fcf848772bca8cc9e358cd57ae19a01f92c9f6f/mkdocs_include_markdown_plugin-7.1.6-py3-none-any.whl", hash = "sha256:7975a593514887c18ecb68e11e35c074c5499cfa3e51b18cd16323862e1f7345", size = 27161, upload-time = "2025-06-13T18:25:49.847Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs", version = "5.7.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "backrefs", version = "5.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pymdown-extensions", version = "10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/c1/f804ba2db2ddc2183e900befe7dad64339a34fa935034e1ab405289d0a97/mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5", size = 3951836, upload-time = "2025-07-01T10:14:15.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/dda19f0495a9096b64b6b3c07c4bfcff1c76ee0fc521086d53593f18b4c0/mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a", size = 8716840, upload-time = "2025-07-01T10:14:13.18Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "jinja2", marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs", marker = "python_full_version < '3.9'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/170ff04de72227f715d67da32950c7b8434449f3805b2ec3dd1085db4d7c/mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33", size = 92677, upload-time = "2024-09-06T10:26:06.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cc/8ba127aaee5d1e9046b0d33fa5b3d17da95a9d705d44902792e0569257fd/mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf", size = 29643, upload-time = "2024-09-06T10:26:04.498Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "1.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2", marker = "python_full_version >= '3.9'" }, + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs", marker = "python_full_version >= '3.9'" }, + { name = "mkdocs-autorefs", version = "1.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pymdown-extensions", version = "10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "1.16.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/ba/534c934cd0a809f51c91332d6ed278782ee4126b8ba8db02c2003f162b47/mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322", size = 166890, upload-time = "2024-09-03T17:20:54.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f2/2a2c48fda645ac6bbe73bcc974587a579092b6868e6ff8bc6d177f4db38a/mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af", size = 109297, upload-time = "2024-09-03T17:20:52.621Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.12" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "griffe", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs-autorefs", version = "1.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocstrings", version = "0.30.0", 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +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 = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pip" +version = "25.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850, upload-time = "2025-02-09T17:14:04.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f", size = 1841526, upload-time = "2025-02-09T17:14:01.463Z" }, +] + +[[package]] +name = "pip" +version = "25.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/59/de/241caa0ca606f2ec5fe0c1f4261b0465df78d786a38da693864a116c37f4/pip-25.1.1.tar.gz", hash = "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077", size = 1940155, upload-time = "2025-05-02T15:14:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", size = 1825227, upload-time = "2025-05-02T15:13:59.102Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "plumbum" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "python_full_version >= '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083, upload-time = "2024-10-05T05:59:27.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/9d/d03542c93bb3d448406731b80f39c3d5601282f778328c22c77d270f4ed4/plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5", size = 127970, upload-time = "2024-10-05T05:59:25.102Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "python_full_version >= '3.11'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677, upload-time = "2025-04-23T18:32:27.227Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735, upload-time = "2025-04-23T18:32:29.019Z" }, + { url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467, upload-time = "2025-04-23T18:32:31.119Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041, upload-time = "2025-04-23T18:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503, upload-time = "2025-04-23T18:32:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079, upload-time = "2025-04-23T18:32:37.659Z" }, + { url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508, upload-time = "2025-04-23T18:32:39.637Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693, upload-time = "2025-04-23T18:32:41.818Z" }, + { url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224, upload-time = "2025-04-23T18:32:44.033Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403, upload-time = "2025-04-23T18:32:45.836Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331, upload-time = "2025-04-23T18:32:47.618Z" }, + { url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571, upload-time = "2025-04-23T18:32:49.401Z" }, + { url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504, upload-time = "2025-04-23T18:32:51.287Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, + { url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034, upload-time = "2025-04-23T18:33:32.843Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578, upload-time = "2025-04-23T18:33:34.912Z" }, + { url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858, upload-time = "2025-04-23T18:33:36.933Z" }, + { url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498, upload-time = "2025-04-23T18:33:38.997Z" }, + { url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428, upload-time = "2025-04-23T18:33:41.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854, upload-time = "2025-04-23T18:33:43.446Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859, upload-time = "2025-04-23T18:33:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059, upload-time = "2025-04-23T18:33:47.735Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyyaml", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/92/a7296491dbf5585b3a987f3f3fc87af0e632121ff3e490c14b5f2d2b4eb5/pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7", size = 852320, upload-time = "2025-04-27T23:48:29.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d1/c54e608505776ce4e7966d03358ae635cfd51dff1da6ee421c090dbc797b/pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f", size = 265845, upload-time = "2025-04-27T23:48:27.359Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyyaml", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "iniconfig", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "iniconfig", marker = "python_full_version >= '3.9'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pygments", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/75/20/6cd04d636a4c83458ecbb7c8220c13786a1a80d3f5fb568df39310e73e98/pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c", size = 8766775, upload-time = "2025-07-14T20:12:55.029Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/94c10268bae5d0d0c6509bdfb5aa08882d11a9ccdf89ff1cde59a6161afb/pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd", size = 9594743, upload-time = "2025-07-14T20:12:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218, upload-time = "2024-08-06T20:33:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067, upload-time = "2024-08-06T20:33:07.879Z" }, + { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812, upload-time = "2024-08-06T20:33:12.542Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531, upload-time = "2024-08-06T20:33:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820, upload-time = "2024-08-06T20:33:16.586Z" }, + { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514, upload-time = "2024-08-06T20:33:22.414Z" }, + { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702, upload-time = "2024-08-06T20:33:23.813Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "pyyaml", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload-time = "2020-11-12T02:38:26.239Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload-time = "2020-11-12T02:38:24.638Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "pyyaml", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "questionary" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775, upload-time = "2024-12-29T11:49:17.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747, upload-time = "2024-12-29T11:49:16.734Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, +] + +[[package]] +name = "rich-argparse" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a6/34460d81e5534f6d2fc8e8d91ff99a5835fdca53578eac89e4f37b3a7c6d/rich_argparse-1.7.1.tar.gz", hash = "sha256:d7a493cde94043e41ea68fb43a74405fa178de981bf7b800f7a3bd02ac5c27be", size = 38094, upload-time = "2025-05-25T20:20:35.335Z" } +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 = "setuptools" +version = "75.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/01/771ea46cce201dd42cff043a5eea929d1c030fb3d1c2ee2729d02ca7814c/setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5", size = 1354489, upload-time = "2025-03-12T00:02:19.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/65/3f0dba35760d902849d39d38c0a72767794b1963227b69a587f8a336d08c/setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9", size = 1251198, upload-time = "2025-03-12T00:02:17.554Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "setuptools-scm" +source = { editable = "." } +dependencies = [ + { name = "packaging" }, + { name = "setuptools", version = "75.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "setuptools", version = "80.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { 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.*'" }, +] + +[package.optional-dependencies] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-entangled-plugin", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "mkdocs-entangled-plugin", version = "0.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "mkdocs-include-markdown-plugin", version = "6.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs-include-markdown-plugin", version = "7.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version < '3.9'" }, + { name = "mkdocstrings", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.9'" }, + { name = "pygments" }, +] +rich = [ + { name = "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'" }, + { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "rich" }, + { 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" }, +] + +[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'" }, + { name = "mkdocs-material", marker = "extra == 'docs'" }, + { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'" }, + { name = "packaging", specifier = ">=20" }, + { name = "pip", marker = "extra == 'test'" }, + { name = "pygments", marker = "extra == 'docs'" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "rich", marker = "extra == 'rich'" }, + { name = "rich", marker = "extra == 'test'" }, + { name = "setuptools" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' and extra == 'test'" }, + { name = "wheel", marker = "extra == 'test'" }, +] +provides-extras = ["docs", "rich", "test", "toml"] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/ab/18f4c8f2bec75eb1a7aebcc52cdb02ab04fd39ff7025bb1b1c7846cc45b8/tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c", size = 191420, upload-time = "2024-05-08T13:50:19.363Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/6d/b5406752c4e4ba86692b22fab0afed8b48f16bdde8f92e1d852976b61dc6/tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f", size = 37685, upload-time = "2024-05-08T13:50:17.343Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "watchdog" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/a6/d6ef450393dac5734c63c40a131f66808d2e6f59f6165ab38c98fbe4e6ec/watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9", size = 124593, upload-time = "2023-03-20T09:21:11.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/fd/58b82550ebe4883bb2a5e1b6c14d8702b5ce0f36c58470bba51dc777df46/watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41", size = 100697, upload-time = "2023-03-20T09:20:25.047Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/42f47ffdfadff4c41b89c54163f323f875eb963bf90088e477c43b8f7b15/watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397", size = 91219, upload-time = "2023-03-20T09:20:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/39/30bb3c2e4f8e89b5c60e98589acf5c5a001cb0efde249aa05d748d1734a2/watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96", size = 91756, upload-time = "2023-03-20T09:20:28.309Z" }, + { url = "https://files.pythonhosted.org/packages/00/9e/a9711f35f1ad6571e92dc2e955e7de9dfac21a1b33e9cd212f066a60a387/watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae", size = 100700, upload-time = "2023-03-20T09:20:29.847Z" }, + { url = "https://files.pythonhosted.org/packages/84/ab/67001e62603bf2ea35ace40023f7c74f61e8b047160d6bb078373cec1a67/watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9", size = 91251, upload-time = "2023-03-20T09:20:31.892Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/d419fdbd3051b42b0a8091ddf78f70540b6d9d277a84845f7c5955f9de92/watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7", size = 91753, upload-time = "2023-03-20T09:20:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6e/7ca8ed16928d7b11da69372f55c64a09dce649d2b24b03f7063cd8683c4b/watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f", size = 100655, upload-time = "2023-03-20T09:20:37.473Z" }, + { url = "https://files.pythonhosted.org/packages/2e/54/48527f3aea4f7ed331072352fee034a7f3d6ec7a2ed873681738b2586498/watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc", size = 91216, upload-time = "2023-03-20T09:20:39.793Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/3a3ce6dd01807ff918aec3bbcabc92ed1a7edc5bb2266c720bb39fec1bec/watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3", size = 91752, upload-time = "2023-03-20T09:20:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/75/fe/d9a37d8df76878853f68dd665ec6d2c7a984645de460164cb880a93ffe6b/watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3", size = 100653, upload-time = "2023-03-20T09:20:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/94/ce/70c65a6c4b0330129c402624d42f67ce82d6a0ba2036de67628aeffda3c1/watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0", size = 91247, upload-time = "2023-03-20T09:20:45.157Z" }, + { url = "https://files.pythonhosted.org/packages/51/b9/444a984b1667013bac41b31b45d9718e069cc7502a43a924896806605d83/watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8", size = 91753, upload-time = "2023-03-20T09:20:46.913Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/bef1c6f6ac18041234a9f3e8bc995d611e255c44f10433bfaf255968c269/watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346", size = 90419, upload-time = "2023-03-20T09:20:50.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/9e36a3c821d47a22e54a8fc73681586b2d26e82d24ea3af63acf2ef78f97/watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64", size = 90428, upload-time = "2023-03-20T09:20:52.216Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/631872d7fbc45527037060db8c838b47a129a6c09d2297d6dddcfa283cf2/watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a", size = 82049, upload-time = "2023-03-20T09:20:53.951Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/4e3230bdc1fb878b152a2c66aa941732776f4545bd68135d490591d66713/watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44", size = 82049, upload-time = "2023-03-20T09:20:55.583Z" }, + { url = "https://files.pythonhosted.org/packages/21/72/46fd174352cd88b9157ade77e3b8835125d4b1e5186fc7f1e8c44664e029/watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a", size = 82052, upload-time = "2023-03-20T09:20:57.124Z" }, + { url = "https://files.pythonhosted.org/packages/74/3c/e4b77f4f069aca2b6e35925db7a1aa6cb600dcb52fc3e962284640ca37f3/watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709", size = 82050, upload-time = "2023-03-20T09:20:58.864Z" }, + { url = "https://files.pythonhosted.org/packages/71/3a/b12740f4f60861240d57b42a2ac6ac0a2821db506c4435f7872c1fad867d/watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83", size = 82050, upload-time = "2023-03-20T09:21:00.452Z" }, + { url = "https://files.pythonhosted.org/packages/40/1b/4e6d3e0f587587931f590531b4ed08070d71a9efb35541d792a68d8ee593/watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d", size = 82049, upload-time = "2023-03-20T09:21:01.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f0/456948b865ab259784f774154e7d65844fa9757522fdb11533fbf8ae7aca/watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33", size = 82051, upload-time = "2023-03-20T09:21:03.67Z" }, + { url = "https://files.pythonhosted.org/packages/55/0d/bfc2a0d425b12444a2dc245a934c065bbb7bd9833fff071cba79c21bb76e/watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f", size = 82038, upload-time = "2023-03-20T09:21:05.492Z" }, + { url = "https://files.pythonhosted.org/packages/9b/6e/ce8d124d03cd3f2941365d9c81d62e3afe43f2dc7e6e86274fa9c2ec2d5b/watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c", size = 82040, upload-time = "2023-03-20T09:21:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/cd0337069c468f22ef256e768ece74c78b511092f1004ab260268e1af4a9/watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759", size = 82040, upload-time = "2023-03-20T09:21:09.178Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "bracex", version = "2.5.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/ab/b3a52228538ccb983653c446c1656eddf1d5303b9cb8b9aef6a91299f862/wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a", size = 115578, upload-time = "2024-09-26T18:39:52.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/df/4ee467ab39cc1de4b852c212c1ed3becfec2e486a51ac1ce0091f85f38d7/wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a", size = 39347, upload-time = "2024-09-26T18:39:51.002Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "bracex", version = "2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 6d3524fc04a979c89ce00e3345f53d89efd055d0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 21:31:50 +0200 Subject: [PATCH 030/162] add changelog for the fix of #1099 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc76b583..565245b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,15 @@ - 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 ### Changed - add `pip` to test optional dependencies for improved uv venv compatibility - migrate to selectable entrypoints for better extensibility - 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 ### Fixed From 8b16ce991e97c1d51e3187acf83f94c2fe125ac7 Mon Sep 17 00:00:00 2001 From: JulianFP Date: Thu, 24 Jul 2025 21:04:45 +0200 Subject: [PATCH 031/162] Add commit_id to dump_file function in addition to version --- .../_integration/dump_version.py | 8 +++++- testing/test_basic_api.py | 25 ++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py index a7bfcae7..9e2c4617 100644 --- a/src/setuptools_scm/_integration/dump_version.py +++ b/src/setuptools_scm/_integration/dump_version.py @@ -16,7 +16,7 @@ # file generated by setuptools-scm # don't change, don't track in version control -__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple", "__commit_id__", "commit_id"] TYPE_CHECKING = False if TYPE_CHECKING: @@ -24,16 +24,22 @@ from typing import Union VERSION_TUPLE = Tuple[Union[int, str], ...] + COMMIT_ID = Union[str, None] else: VERSION_TUPLE = object + COMMIT_ID = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE +commit_id: COMMIT_ID +__commit_id__: COMMIT_ID __version__ = version = {version!r} __version_tuple__ = version_tuple = {version_tuple!r} + +__commit_id__ = commit_id = {scm_version.node!r} """, ".txt": "{version}", } diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py index 76239841..dd7224e5 100644 --- a/testing/test_basic_api.py +++ b/testing/test_basic_api.py @@ -184,17 +184,30 @@ def read(name: str) -> str: scm_version = meta("1.0", distance=42, config=c) dump_version(tmp_path, version, "first.py", scm_version=scm_version) lines = read("first.py").splitlines() - assert lines[-2:] == [ + assert lines[-4:] == [ "__version__ = version = '1.0.dev42'", "__version_tuple__ = version_tuple = (1, 0, 'dev42')", + "", + "__commit_id__ = commit_id = None" + ] + + version = "1.0.1" + scm_version = meta("1.0.1", node="g4ac9d2c", config=c) + dump_version(tmp_path, version, "second.py", scm_version=scm_version) + lines = read("second.py").splitlines() + assert lines[-4:] == [ + "__version__ = version = '1.0.1'", + "__version_tuple__ = version_tuple = (1, 0, 1)", + "", + "__commit_id__ = commit_id = 'g4ac9d2c'" ] version = "1.0.1+g4ac9d2c" scm_version = meta("1.0.1", node="g4ac9d2c", config=c) dump_version( - tmp_path, version, "second.py", scm_version=scm_version, template=template + tmp_path, version, "third.py", scm_version=scm_version, template=template ) - lines = read("second.py").splitlines() + lines = read("third.py").splitlines() assert "__version__ = version = '1.0.1+g4ac9d2c'" in lines assert "__version_tuple__ = version_tuple = (1, 0, 1, 'g4ac9d2c')" in lines assert "__sha__ = 'g4ac9d2c'" in lines @@ -204,9 +217,9 @@ def read(name: str) -> str: "1.2.3", node="gb366d8b", distance=18, node_date=date(2021, 4, 15), config=c ) dump_version( - tmp_path, version, "third.py", scm_version=scm_version, template=template + tmp_path, version, "fourth.py", scm_version=scm_version, template=template ) - lines = read("third.py").splitlines() + lines = read("fourth.py").splitlines() assert "__version__ = version = '1.2.3.dev18+gb366d8b.d20210415'" in lines assert ( "__version_tuple__ = version_tuple = (1, 2, 3, 'dev18', 'gb366d8b.d20210415')" @@ -216,7 +229,7 @@ def read(name: str) -> str: import ast - ast.parse(read("third.py")) + ast.parse(read("fourth.py")) def test_parse_plain_fails(recwarn: pytest.WarningsRecorder) -> None: From 9217df2761fc40b64ceee09a7573fe448699c4b2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:26:08 +0000 Subject: [PATCH 032/162] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_basic_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py index dd7224e5..2dbe2292 100644 --- a/testing/test_basic_api.py +++ b/testing/test_basic_api.py @@ -188,7 +188,7 @@ def read(name: str) -> str: "__version__ = version = '1.0.dev42'", "__version_tuple__ = version_tuple = (1, 0, 'dev42')", "", - "__commit_id__ = commit_id = None" + "__commit_id__ = commit_id = None", ] version = "1.0.1" @@ -199,7 +199,7 @@ def read(name: str) -> str: "__version__ = version = '1.0.1'", "__version_tuple__ = version_tuple = (1, 0, 1)", "", - "__commit_id__ = commit_id = 'g4ac9d2c'" + "__commit_id__ = commit_id = 'g4ac9d2c'", ] version = "1.0.1+g4ac9d2c" From 5f68a3c39ca2c34f61940e82e774ccebabc750ec Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 22:02:07 +0200 Subject: [PATCH 033/162] introduce run_hg and remove the need for module reloading for the hg command env var --- src/setuptools_scm/_file_finders/hg.py | 10 +++----- src/setuptools_scm/hg.py | 34 +++++++++++++++++--------- src/setuptools_scm/hg_git.py | 26 +++++++++----------- testing/test_file_finder.py | 5 +--- testing/test_hg_git.py | 9 +------ testing/test_mercurial.py | 12 ++++----- 6 files changed, 46 insertions(+), 50 deletions(-) diff --git a/src/setuptools_scm/_file_finders/hg.py b/src/setuptools_scm/_file_finders/hg.py index 4fc3a1ee..182429c3 100644 --- a/src/setuptools_scm/_file_finders/hg.py +++ b/src/setuptools_scm/_file_finders/hg.py @@ -7,19 +7,17 @@ from .. import _types as _t from .._file_finders import is_toplevel_acceptable from .._file_finders import scm_find_files -from .._run_cmd import run as _run +from ..hg import run_hg from ..integration import data_from_mime from .pathtools import norm_real log = logging.getLogger(__name__) -HG_COMMAND = os.environ.get("SETUPTOOLS_SCM_HG_COMMAND", "hg") - def _hg_toplevel(path: str) -> str | None: try: - return _run( - [HG_COMMAND, "root"], + return run_hg( + ["root"], cwd=(path or "."), check=True, ).parse_success(norm_real) @@ -34,7 +32,7 @@ def _hg_toplevel(path: str) -> str | None: def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: hg_files: set[str] = set() hg_dirs = {toplevel} - res = _run([HG_COMMAND, "files"], cwd=toplevel) + res = run_hg(["files"], cwd=toplevel) if res.returncode: return set(), set() for name in res.stdout.splitlines(): diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index 238da909..bfdfd425 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import TYPE_CHECKING +from typing import Any from . import Configuration from ._version_cls import Version @@ -19,18 +20,28 @@ if TYPE_CHECKING: from . import _types as _t +from ._run_cmd import CompletedProcess from ._run_cmd import require_command as _require_command from ._run_cmd import run as _run log = logging.getLogger(__name__) -HG_COMMAND = os.environ.get("SETUPTOOLS_SCM_HG_COMMAND", "hg") + +def _get_hg_command() -> str: + """Get the hg command from environment, allowing runtime configuration.""" + return os.environ.get("SETUPTOOLS_SCM_HG_COMMAND", "hg") + + +def run_hg(args: list[str], cwd: _t.PathT, **kwargs: Any) -> CompletedProcess: + """Run mercurial command with the configured hg executable.""" + cmd = [_get_hg_command(), *args] + return _run(cmd, cwd=cwd, **kwargs) class HgWorkdir(Workdir): @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None: - res = _run([HG_COMMAND, "root"], wd) + res = run_hg(["root"], wd) if res.returncode: return None return cls(Path(res.stdout)) @@ -79,8 +90,8 @@ def _get_node_info(self) -> tuple[str, str, str] | None: def _get_branch_info(self) -> tuple[str, bool, str]: """Get branch name, dirty status, and dirty date.""" - branch, dirty_str, dirty_date = _run( - [HG_COMMAND, "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"], + branch, dirty_str, dirty_date = run_hg( + ["id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"], cwd=self.path, check=True, ).stdout.split("\n") @@ -181,9 +192,9 @@ def _get_distance_based_version( return None def hg_log(self, revset: str, template: str) -> str: - cmd = [HG_COMMAND, "log", "-r", revset, "-T", template] - - return _run(cmd, cwd=self.path, check=True).stdout + return run_hg( + ["log", "-r", revset, "-T", template], cwd=self.path, check=True + ).stdout def get_latest_normalizable_tag(self) -> str | None: # Gets all tags containing a '.' (see #229) from oldest to newest @@ -223,12 +234,12 @@ def get_dirty_tag_date(self) -> datetime.date | None: """ try: # Check if working directory is dirty first - res = _run([HG_COMMAND, "id", "-T", "{dirty}"], cwd=self.path) + res = run_hg(["id", "-T", "{dirty}"], cwd=self.path) if res.returncode != 0 or not bool(res.stdout): return None # Get list of changed files using hg status - status_res = _run([HG_COMMAND, "status", "-m", "-a", "-r"], cwd=self.path) + status_res = run_hg(["status", "-m", "-a", "-r"], cwd=self.path) if status_res.returncode != 0: return None @@ -248,9 +259,10 @@ def get_dirty_tag_date(self) -> datetime.date | None: def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: - _require_command(HG_COMMAND) + hg_cmd = _get_hg_command() + _require_command(hg_cmd) if os.path.exists(os.path.join(root, ".hg/git")): - res = _run([HG_COMMAND, "path"], root) + res = run_hg(["path"], root) if not res.returncode: for line in res.stdout.split("\n"): if line.startswith("default ="): diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index f040eb9e..47b48954 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -9,10 +9,9 @@ from . import _types as _t from ._run_cmd import CompletedProcess as _CompletedProcess -from ._run_cmd import run as _run from .git import GitWorkdir -from .hg import HG_COMMAND from .hg import HgWorkdir +from .hg import run_hg from .scm_workdir import get_latest_file_mtime log = logging.getLogger(__name__) @@ -27,25 +26,25 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir): @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None: - res = _run([HG_COMMAND, "root"], cwd=wd).parse_success(parse=Path) + res = run_hg(["root"], cwd=wd).parse_success(parse=Path) if res is None: return None return cls(res) def is_dirty(self) -> bool: - res = _run([HG_COMMAND, "id", "-T", "{dirty}"], cwd=self.path, check=True) + res = run_hg(["id", "-T", "{dirty}"], cwd=self.path, check=True) return bool(res.stdout) def get_branch(self) -> str | None: - res = _run([HG_COMMAND, "id", "-T", "{bookmarks}"], cwd=self.path) + res = run_hg(["id", "-T", "{bookmarks}"], cwd=self.path) if res.returncode: log.info("branch err %s", res) return None return res.stdout def get_head_date(self) -> date | None: - return _run( - [HG_COMMAND, "log", "-r", ".", "-T", "{shortdate(date)}"], cwd=self.path + return run_hg( + ["log", "-r", ".", "-T", "{shortdate(date)}"], cwd=self.path ).parse_success(parse=date.fromisoformat, error_msg="head date err") def get_dirty_tag_date(self) -> date | None: @@ -59,7 +58,7 @@ def get_dirty_tag_date(self) -> date | None: try: # Get list of changed files using hg status - status_res = _run([HG_COMMAND, "status", "-m", "-a", "-r"], cwd=self.path) + status_res = run_hg(["status", "-m", "-a", "-r"], cwd=self.path) if status_res.returncode != 0: return None @@ -84,7 +83,7 @@ def fetch_shallow(self) -> None: pass def get_hg_node(self) -> str | None: - res = _run([HG_COMMAND, "log", "-r", ".", "-T", "{node}"], cwd=self.path) + res = run_hg(["log", "-r", ".", "-T", "{node}"], cwd=self.path) if res.returncode: return None else: @@ -108,7 +107,7 @@ def node(self) -> str | None: if git_node is None: # trying again after hg -> git - _run([HG_COMMAND, "gexport"], cwd=self.path) + run_hg(["gexport"], cwd=self.path) git_node = self._hg2git(hg_node) if git_node is None: @@ -123,7 +122,7 @@ def node(self) -> str | None: return git_node[:7] def count_all_nodes(self) -> int: - res = _run([HG_COMMAND, "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) + res = run_hg(["log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) return len(res.stdout) def default_describe(self) -> _CompletedProcess: @@ -133,9 +132,8 @@ def default_describe(self) -> _CompletedProcess: `git describe --dirty --tags --long --match *[0-9]*` """ - res = _run( + res = run_hg( [ - HG_COMMAND, "log", "-r", "(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))", @@ -163,7 +161,7 @@ def default_describe(self) -> _CompletedProcess: logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags) return _FAKE_GIT_DESCRIBE_ERROR - res = _run([HG_COMMAND, "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path) + res = run_hg(["log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path) if res.returncode: return _FAKE_GIT_DESCRIBE_ERROR distance = len(res.stdout) - 1 diff --git a/testing/test_file_finder.py b/testing/test_file_finder.py index 9daf9b04..7d31e7d1 100644 --- a/testing/test_file_finder.py +++ b/testing/test_file_finder.py @@ -1,6 +1,5 @@ from __future__ import annotations -import importlib import os import sys @@ -9,7 +8,6 @@ import pytest from setuptools_scm._file_finders import find_files -from setuptools_scm._file_finders import hg from .wd_wrapper import WorkDir @@ -275,6 +273,5 @@ def test_hg_command_from_env( with monkeypatch.context() as m: m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) m.setenv("PATH", str(hg_wd.cwd / "not-existing")) - request.addfinalizer(lambda: importlib.reload(hg)) - importlib.reload(hg) + # No module reloading needed - runtime configuration works immediately assert set(find_files()) == {"file"} diff --git a/testing/test_hg_git.py b/testing/test_hg_git.py index f2a9539f..1c9101a6 100644 --- a/testing/test_hg_git.py +++ b/testing/test_hg_git.py @@ -1,12 +1,8 @@ from __future__ import annotations -import importlib - import pytest from setuptools_scm import Configuration -from setuptools_scm import hg -from setuptools_scm import hg_git from setuptools_scm._run_cmd import CommandNotFoundError from setuptools_scm._run_cmd import has_command from setuptools_scm._run_cmd import run @@ -113,9 +109,6 @@ def test_hg_command_from_env( with monkeypatch.context() as m: m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) m.setenv("PATH", str(wd.cwd / "not-existing")) - request.addfinalizer(lambda: importlib.reload(hg)) - request.addfinalizer(lambda: importlib.reload(hg_git)) - importlib.reload(hg) - importlib.reload(hg_git) + # No module reloading needed - runtime configuration works immediately wd.write("pyproject.toml", "[tool.setuptools_scm]") assert wd.get_version().startswith("0.1.dev0+") diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 6c2a137c..23f9c695 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -1,6 +1,5 @@ from __future__ import annotations -import importlib import os from pathlib import Path @@ -10,7 +9,6 @@ import setuptools_scm._file_finders from setuptools_scm import Configuration -from setuptools_scm import hg from setuptools_scm._run_cmd import CommandNotFoundError from setuptools_scm._run_cmd import has_command from setuptools_scm.hg import archival_to_version @@ -78,10 +76,11 @@ def test_hg_command_from_env( with monkeypatch.context() as m: m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) m.setenv("PATH", str(wd.cwd / "not-existing")) - request.addfinalizer(lambda: importlib.reload(hg)) - importlib.reload(hg) wd.write("pyproject.toml", "[tool.setuptools_scm]") - assert wd.get_version() == "0.0" + # Need to commit something first for versioning to work + wd.commit_testfile() + version = wd.get_version() + assert version.startswith("0.1.dev1+") def test_hg_command_from_env_is_invalid( @@ -89,8 +88,7 @@ def test_hg_command_from_env_is_invalid( ) -> None: with monkeypatch.context() as m: m.setenv("SETUPTOOLS_SCM_HG_COMMAND", str(wd.cwd / "not-existing")) - request.addfinalizer(lambda: importlib.reload(hg)) - importlib.reload(hg) + # No module reloading needed - runtime configuration works immediately config = Configuration() wd.write("pyproject.toml", "[tool.setuptools_scm]") with pytest.raises(CommandNotFoundError, match=r"hg"): From 48d2fd7571ac1b1fde826ceb5b37e17a0378264d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 22:13:46 +0200 Subject: [PATCH 034/162] use correct hg path in the isolated from env test --- testing/test_mercurial.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 23f9c695..f931a46c 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -77,6 +77,9 @@ def test_hg_command_from_env( m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) m.setenv("PATH", str(wd.cwd / "not-existing")) wd.write("pyproject.toml", "[tool.setuptools_scm]") + # Use the configured hg command for test operations + wd.add_command = f"{hg_exe} add ." + wd.commit_command = f'{hg_exe} commit -m test-{{reason}} -u test -d "0 0"' # Need to commit something first for versioning to work wd.commit_testfile() version = wd.get_version() From 75869c1c9be404e22a5afeed9c11adbdf7a46dca Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Jul 2025 22:17:58 +0200 Subject: [PATCH 035/162] dont depend on mercurial on pypy for test --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7dc6210b..291ec5ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ test = [ "rich", 'typing-extensions; python_version < "3.11"', "wheel", - "mercurial" + 'mercurial; platform_python_implementation != "PyPy"' ] toml = [ ] From 850162f0305fc90763dab1b06b1029a1b39cb74d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 26 Jul 2025 11:16:35 +0200 Subject: [PATCH 036/162] fixup: enhance tests for hg command override to work on windows --- testing/test_mercurial.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index f931a46c..1ebd1e7a 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -73,17 +73,14 @@ def test_hg_command_from_env( request: pytest.FixtureRequest, hg_exe: str, ) -> None: - with monkeypatch.context() as m: - m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) - m.setenv("PATH", str(wd.cwd / "not-existing")) - wd.write("pyproject.toml", "[tool.setuptools_scm]") - # Use the configured hg command for test operations - wd.add_command = f"{hg_exe} add ." - wd.commit_command = f'{hg_exe} commit -m test-{{reason}} -u test -d "0 0"' - # Need to commit something first for versioning to work - wd.commit_testfile() - version = wd.get_version() - assert version.startswith("0.1.dev1+") + wd.write("pyproject.toml", "[tool.setuptools_scm]") + # Need to commit something first for versioning to work + wd.commit_testfile() + + monkeypatch.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) + monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) + version = wd.get_version() + assert version.startswith("0.1.dev1+") def test_hg_command_from_env_is_invalid( @@ -94,7 +91,7 @@ def test_hg_command_from_env_is_invalid( # No module reloading needed - runtime configuration works immediately config = Configuration() wd.write("pyproject.toml", "[tool.setuptools_scm]") - with pytest.raises(CommandNotFoundError, match=r"hg"): + with pytest.raises(CommandNotFoundError, match=r"test.*hg.*not-existing"): parse(wd.cwd, config=config) assert wd.get_version(fallback_version="1.0") == "1.0" From d9a92fdaa121653fd36841792a25762342044568 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 26 Jul 2025 14:42:07 +0200 Subject: [PATCH 037/162] drop mercurial test dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 291ec5ed..a280e3fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ test = [ "rich", 'typing-extensions; python_version < "3.11"', "wheel", - 'mercurial; platform_python_implementation != "PyPy"' ] toml = [ ] From e4cd419a988e86c4ec075fab6908f47a8b21becc Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 27 Jul 2025 23:37:13 +0200 Subject: [PATCH 038/162] 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 039/162] 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 040/162] 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 041/162] 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'" }, From 9f6326266fc1c4ef160d3f04dadde1d5d67ea647 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 28 Jul 2025 14:52:10 +0200 Subject: [PATCH 042/162] experiment with griffe as api stability check/followup --- .github/workflows/api-check.yml | 38 ++++++ pyproject.toml | 2 + uv.lock | 217 ++++++++++++++++++++++++++++---- 3 files changed, 233 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/api-check.yml diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml new file mode 100644 index 00000000..97f500dd --- /dev/null +++ b/.github/workflows/api-check.yml @@ -0,0 +1,38 @@ +name: API Stability Check + +on: + pull_request: + push: + branches: + - "*" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + api-check: + name: Check API stability with griffe + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -U pip setuptools + pip install -e .[test] + + - name: Run griffe API check + run: | + griffe check setuptools_scm -ssrc -f github \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a280e3fc..f6960ea9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,8 @@ test = [ "rich", 'typing-extensions; python_version < "3.11"', "wheel", + "griffe", + "flake8", ] toml = [ ] diff --git a/uv.lock b/uv.lock index b5aa1c46..81291d03 100644 --- a/uv.lock +++ b/uv.lock @@ -5,7 +5,8 @@ resolution-markers = [ "python_full_version >= '3.11'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] [[package]] @@ -56,7 +57,8 @@ name = "backrefs" version = "5.7.post1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/df/30/903f35159c87ff1d92aa3fcf8cb52de97632a21e0ae43ed940f5d033e01a/backrefs-5.7.post1.tar.gz", hash = "sha256:8b0f83b770332ee2f1c8244f4e03c77d127a0fa529328e6a0e77fa25bee99678", size = 6582270, upload-time = "2024-06-16T18:38:20.166Z" } wheels = [ @@ -91,7 +93,8 @@ name = "bracex" version = "2.5.post1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641, upload-time = "2024-09-28T21:41:22.017Z" } wheels = [ @@ -245,7 +248,8 @@ version = "8.1.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, @@ -360,6 +364,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] +[[package]] +name = "flake8" +version = "5.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version < '3.8.1'" }, + { name = "pycodestyle", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, + { name = "pyflakes", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/00/9808c62b2d529cefc69ce4e4a1ea42c0f855effa55817b7327ec5b75e60a/flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", size = 145862, upload-time = "2022-08-03T23:21:27.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248", size = 61897, upload-time = "2022-08-03T23:21:25.027Z" }, +] + +[[package]] +name = "flake8" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "pycodestyle", version = "2.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "pyflakes", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload-time = "2025-02-16T18:45:44.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload-time = "2025-02-16T18:45:42.351Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version >= '3.9'" }, + { name = "pycodestyle", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyflakes", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + [[package]] name = "funcy" version = "2.0" @@ -386,7 +443,8 @@ name = "griffe" version = "1.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "astunparse", marker = "python_full_version < '3.9'" }, @@ -428,7 +486,8 @@ name = "importlib-metadata" version = "8.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -494,7 +553,8 @@ name = "markdown" version = "3.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -538,7 +598,8 @@ name = "markupsafe" version = "2.1.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } wheels = [ @@ -676,6 +737,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/29/b4ddddd1ae74ce2651e7eedf1871a6b2240433eeec070622f81b985d5e70/mawk-0.1.4-py3-none-any.whl", hash = "sha256:8ab7ce0808d10769f8aa05af8448046c290af5f529db874b44a8fd56056c4462", size = 7941, upload-time = "2023-06-04T12:53:41.112Z" }, ] +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -729,7 +799,8 @@ name = "mkdocs-autorefs" version = "1.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -767,7 +838,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "mkdocs", marker = "python_full_version < '3.11'" }, @@ -816,7 +888,8 @@ name = "mkdocs-include-markdown-plugin" version = "6.2.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "mkdocs", marker = "python_full_version < '3.9'" }, @@ -884,7 +957,8 @@ name = "mkdocstrings" version = "0.26.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -941,7 +1015,8 @@ name = "mkdocstrings-python" version = "1.11.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -1017,7 +1092,8 @@ name = "pip" version = "25.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850, upload-time = "2025-02-09T17:14:04.423Z" } wheels = [ @@ -1043,7 +1119,8 @@ name = "platformdirs" version = "4.3.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } wheels = [ @@ -1069,7 +1146,8 @@ name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ @@ -1123,6 +1201,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] +[[package]] +name = "pycodestyle" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/83/5bcaedba1f47200f0665ceb07bcb00e2be123192742ee0edfb66b600e5fd/pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", size = 102127, upload-time = "2022-08-03T23:13:29.715Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b", size = 41493, upload-time = "2022-08-03T23:13:27.416Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload-time = "2024-08-04T20:26:54.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload-time = "2024-08-04T20:26:53.173Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -1247,6 +1363,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" }, ] +[[package]] +name = "pyflakes" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/07/92/f0cb5381f752e89a598dd2850941e7f570ac3cb8ea4a344854de486db152/pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3", size = 66388, upload-time = "2022-07-30T17:29:05.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", size = 66116, upload-time = "2022-07-30T17:29:04.179Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -1261,7 +1415,8 @@ name = "pymdown-extensions" version = "10.15" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -1304,7 +1459,8 @@ name = "pytest" version = "8.3.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, @@ -1455,7 +1611,8 @@ name = "pyyaml-env-tag" version = "0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "pyyaml", marker = "python_full_version < '3.9'" }, @@ -1542,7 +1699,8 @@ name = "setuptools" version = "75.3.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/5c/01/771ea46cce201dd42cff043a5eea929d1c030fb3d1c2ee2729d02ca7814c/setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5", size = 1354489, upload-time = "2025-03-12T00:02:19.004Z" } wheels = [ @@ -1592,6 +1750,11 @@ rich = [ ] test = [ { name = "build" }, + { name = "flake8", version = "5.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, + { name = "flake8", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { 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 = "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'" }, @@ -1605,6 +1768,8 @@ test = [ [package.metadata] requires-dist = [ { name = "build", marker = "extra == 'test'" }, + { name = "flake8", marker = "extra == 'test'" }, + { name = "griffe", marker = "extra == 'test'" }, { name = "mkdocs", marker = "extra == 'docs'" }, { name = "mkdocs-entangled-plugin", marker = "extra == 'docs'" }, { name = "mkdocs-include-markdown-plugin", marker = "extra == 'docs'" }, @@ -1686,7 +1851,8 @@ name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } wheels = [ @@ -1724,7 +1890,8 @@ name = "urllib3" version = "2.2.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } wheels = [ @@ -1782,7 +1949,8 @@ name = "wcmatch" version = "10.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] dependencies = [ { name = "bracex", version = "2.5.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -1832,7 +2000,8 @@ name = "zipp" version = "3.20.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.9'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", ] sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" } wheels = [ From d1ca1e321fec9675e83bb41bd4c33d53d0b139d7 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 28 Jul 2025 15:58:41 +0200 Subject: [PATCH 043/162] fix #1045: reindent the __all__ in the version template --- src/setuptools_scm/_integration/dump_version.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py index 9e2c4617..9f53179a 100644 --- a/src/setuptools_scm/_integration/dump_version.py +++ b/src/setuptools_scm/_integration/dump_version.py @@ -16,7 +16,14 @@ # file generated by setuptools-scm # don't change, don't track in version control -__all__ = ["__version__", "__version_tuple__", "version", "version_tuple", "__commit_id__", "commit_id"] +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] TYPE_CHECKING = False if TYPE_CHECKING: From 895376dff1cf695b5c0cbc8330a82f487939acc1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 28 Jul 2025 16:21:15 +0200 Subject: [PATCH 044/162] fix 879: expand windows case handling --- .cursor/rules/test-running.mdc | 8 ++++ CHANGELOG.md | 1 + testing/test_regressions.py | 75 ++++++++++++++++++++++++++++++++++ testing/test_version.py | 6 +-- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 .cursor/rules/test-running.mdc diff --git a/.cursor/rules/test-running.mdc b/.cursor/rules/test-running.mdc new file mode 100644 index 00000000..8c6875c9 --- /dev/null +++ b/.cursor/rules/test-running.mdc @@ -0,0 +1,8 @@ +--- +description: run tests wit uv tooling +globs: +alwaysApply: true +--- + +use `uv run pytest` to run tests +use uv to manage dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d838f4..c16454c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - fix #1136: update customizing.md to fix missing import - fix #1001: document the missing version schemes and add examples in the docs - fix #1115: explicitly document file finder behaviour +- fix #879: add test that validates caswe differenct behavior ## v8.3.1 diff --git a/testing/test_regressions.py b/testing/test_regressions.py index 21f52711..679365e6 100644 --- a/testing/test_regressions.py +++ b/testing/test_regressions.py @@ -101,6 +101,81 @@ def test_case_mismatch_on_windows_git(tmp_path: Path) -> None: assert res is not None +@pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows") +def test_case_mismatch_nested_dir_windows_git(tmp_path: Path) -> None: + """Test case where we have a nested directory with different casing""" + # Create git repo in my_repo + repo_path = tmp_path / "my_repo" + repo_path.mkdir() + run("git init", repo_path) + + # Create a nested directory with specific casing + nested_dir = repo_path / "CasedDir" + nested_dir.mkdir() + + # Create a pyproject.toml in the nested directory + (nested_dir / "pyproject.toml").write_text(""" +[build-system] +requires = ["setuptools>=64", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "test-project" +dynamic = ["version"] + +[tool.setuptools_scm] +""") + + # Add and commit the file + run("git add .", repo_path) + run("git commit -m 'Initial commit'", repo_path) + + # Now try to parse from the nested directory with lowercase path + # This simulates: cd my_repo/caseddir (lowercase) when actual dir is CasedDir + lowercase_nested_path = str(nested_dir).replace("CasedDir", "caseddir") + + # This should trigger the assertion error in _git_toplevel + try: + res = parse(lowercase_nested_path, Configuration()) + # If we get here without assertion error, the bug is already fixed or not triggered + print(f"Parse succeeded with result: {res}") + except AssertionError as e: + print(f"AssertionError caught as expected: {e}") + # Re-raise so the test fails, showing we reproduced the bug + raise + + +def test_case_mismatch_force_assertion_failure(tmp_path: Path) -> None: + """Force the assertion failure by directly calling _git_toplevel with mismatched paths""" + from setuptools_scm._file_finders.git import _git_toplevel + + # Create git repo structure + repo_path = tmp_path / "my_repo" + repo_path.mkdir() + run("git init", repo_path) + + # Create nested directory + nested_dir = repo_path / "CasedDir" + nested_dir.mkdir() + + # Add and commit something to make it a valid repo + (nested_dir / "test.txt").write_text("test") + run("git add .", repo_path) + run("git commit -m 'Initial commit'", repo_path) + + # Now call _git_toplevel with a path that has different casing + # This should cause the assertion to fail + lowercase_nested_path = str(nested_dir).replace("CasedDir", "caseddir") + + try: + result = _git_toplevel(lowercase_nested_path) + print(f"_git_toplevel returned: {result}") + # If no assertion error, either the bug is fixed or we didn't trigger it properly + except AssertionError as e: + print(f"AssertionError as expected: {e}") + raise # Let the test fail to show we reproduced the issue + + def test_entrypoints_load() -> None: d = distribution("setuptools-scm") diff --git a/testing/test_version.py b/testing/test_version.py index 7dce433d..eb31cc1e 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -233,7 +233,7 @@ def test_regex_match_but_no_version() -> None: " however the matched group has no value" ), ): - meta("v1", config=replace(c, tag_regex=re.compile("(?P).*"))) + meta("v1", config=replace(c, tag_regex=re.compile(r"(?P).*"))) @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/471") @@ -482,7 +482,7 @@ def __repr__(self) -> str: def test_no_matching_entrypoints(config_key: str) -> None: version = meta( "1.0", - config=replace(c, **{config_key: "nonexistant"}), # type: ignore + config=replace(c, **{config_key: "nonexistant"}), # type: ignore[arg-type] ) with pytest.raises( ValueError, @@ -499,7 +499,7 @@ def test_all_entrypoints_return_none() -> None: "1.0", config=replace( c, - version_scheme=lambda v: None, # type: ignore + version_scheme=lambda v: None, # type: ignore[arg-type,return-value] ), ) with pytest.raises( From 821817635682e06a3226e828d31019374b19cfa4 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 28 Jul 2025 17:19:26 +0200 Subject: [PATCH 045/162] fix #1052: use consistent node hash length --- .cursor/rules/test-running.mdc | 4 ++ .../_integration/dump_version.py | 22 ++++++++- src/setuptools_scm/_node_utils.py | 46 +++++++++++++++++++ src/setuptools_scm/git.py | 6 +-- src/setuptools_scm/hg.py | 4 +- src/setuptools_scm/hg_git.py | 2 +- src/setuptools_scm/version.py | 3 +- testing/test_mercurial.py | 6 +-- 8 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 src/setuptools_scm/_node_utils.py diff --git a/.cursor/rules/test-running.mdc b/.cursor/rules/test-running.mdc index 8c6875c9..0bd595a2 100644 --- a/.cursor/rules/test-running.mdc +++ b/.cursor/rules/test-running.mdc @@ -6,3 +6,7 @@ alwaysApply: true use `uv run pytest` to run tests use uv to manage dependencies + +follow preexisting conventions in the project + +- use the fixtures \ No newline at end of file diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py index 9f53179a..6918bd19 100644 --- a/src/setuptools_scm/_integration/dump_version.py +++ b/src/setuptools_scm/_integration/dump_version.py @@ -6,11 +6,29 @@ from .. import _types as _t from .._log import log as parent_log +from .._node_utils import _format_node_for_output from .._version_cls import _version_as_tuple from ..version import ScmVersion log = parent_log.getChild("dump_version") + +class _TemplateScmVersion: + """Wrapper for ScmVersion that formats node for template output.""" + + def __init__(self, scm_version: ScmVersion) -> None: + self._scm_version = scm_version + + def __getattr__(self, name: str) -> object: + # Delegate all attribute access to the wrapped ScmVersion + return getattr(self._scm_version, name) + + @property + def node(self) -> str | None: + """Return the node formatted for output.""" + return _format_node_for_output(self._scm_version.node) + + TEMPLATES = { ".py": """\ # file generated by setuptools-scm @@ -101,10 +119,12 @@ def write_version_to_path( log.debug("dump %s into %s", version, target) version_tuple = _version_as_tuple(version) if scm_version is not None: + # Wrap ScmVersion to provide formatted node for templates + template_scm_version = _TemplateScmVersion(scm_version) content = final_template.format( version=version, version_tuple=version_tuple, - scm_version=scm_version, + scm_version=template_scm_version, ) else: content = final_template.format(version=version, version_tuple=version_tuple) diff --git a/src/setuptools_scm/_node_utils.py b/src/setuptools_scm/_node_utils.py new file mode 100644 index 00000000..1a7a2274 --- /dev/null +++ b/src/setuptools_scm/_node_utils.py @@ -0,0 +1,46 @@ +"""Private utilities for consistent node ID handling across SCM backends.""" + +from __future__ import annotations + +# Standard node ID length used across all SCM backends +_NODE_ID_LENGTH = 10 + + +def _slice_node_id(node_id: str) -> str: + """ + Slice a node ID to a consistent length. + + This ensures that all SCM backends (git, mercurial, archival) + return the same length node IDs for consistency. + + Args: + node_id: The full node ID/hash from the SCM + + Returns: + The node ID sliced to the standard length + """ + return node_id[:_NODE_ID_LENGTH] + + +def _format_node_for_output(node_id: str | None) -> str | None: + """ + Format a node ID for output, applying consistent slicing. + + Args: + node_id: The full node ID/hash from the SCM or None + + Returns: + The node ID sliced to standard length for output, or None if input was None + """ + if node_id is None: + return None + + # Handle mercurial nodes with 'h' prefix + if node_id.startswith("h"): + # For mercurial nodes, slice the part after 'h' and reconstruct + hg_hash = node_id[1:] # Remove 'h' prefix + sliced_hash = _slice_node_id(hg_hash) + return "h" + sliced_hash + + # For git nodes (with or without 'g' prefix) and others + return _slice_node_id(node_id) diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 150629b1..7d81d40c 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -2,7 +2,6 @@ import dataclasses import logging -import operator import os import re import shlex @@ -47,6 +46,7 @@ "--dirty", "--tags", "--long", + "--abbrev=40", "--match", "*[0-9]*", ] @@ -174,12 +174,10 @@ def fetch_shallow(self) -> None: run_git(["fetch", "--unshallow"], self.path, check=True, timeout=240) def node(self) -> str | None: - unsafe_short_node = operator.itemgetter(slice(7)) - return run_git( ["rev-parse", "--verify", "--quiet", "HEAD"], self.path ).parse_success( - parse=unsafe_short_node, + parse=str, ) def count_all_nodes(self) -> int: diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index bfdfd425..42320516 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -66,7 +66,7 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: if self._is_initial_node(node): return self._create_initial_meta(config, dirty, branch, node_date) - node = "h" + node[:7] + node = "h" + node tags = self._parse_tags(tags_str) # Try to get version from current tags @@ -285,7 +285,7 @@ def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersion: log.debug("data %s", data) - node = data.get("node", "")[:12] + node = data.get("node", "") if node: node = "h" + node if "tag" in data: diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index 47b48954..3e91b20f 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -119,7 +119,7 @@ def node(self) -> str | None: return hg_node - return git_node[:7] + return git_node def count_all_nodes(self) -> int: res = run_hg(["log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 2ae423ed..c3bc1148 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -16,6 +16,7 @@ from . import _entrypoints from . import _modify_version +from ._node_utils import _format_node_for_output if TYPE_CHECKING: import sys @@ -170,7 +171,7 @@ def format_with(self, fmt: str, **kw: object) -> str: time=self.time, tag=self.tag, distance=self.distance, - node=self.node, + node=_format_node_for_output(self.node), dirty=self.dirty, branch=self.branch, node_date=self.node_date, diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 1ebd1e7a..3e15aae8 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -31,12 +31,12 @@ def wd(wd: WorkDir) -> WorkDir: archival_mapping = { "1.0": {"tag": "1.0"}, - "1.1.0.dev3+h000000000000": { + "1.1.0.dev3+h0000000000": { "latesttag": "1.0", "latesttagdistance": "3", "node": "0" * 20, }, - "1.0.1.dev3+h000000000000": { + "1.0.1.dev3+h0000000000": { "latesttag": "1.0.0", "latesttagdistance": "3", "branch": "1.0", @@ -163,7 +163,7 @@ def test_version_from_archival(wd: WorkDir) -> None: """, ) - assert wd.get_version() == "0.2.dev3+h000000000000" + assert wd.get_version() == "0.2.dev3+h0000000000" @pytest.mark.issue("#72") From ed3f6774a8480f7218cf1b99ce1a158f3741d2d0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 28 Jul 2025 18:26:25 +0200 Subject: [PATCH 046/162] introduce a mechanism to configure git pre-parse+submodule checks fixes #846 --- docs/config.md | 14 ++++ docs/usage.md | 4 + src/setuptools_scm/_config.py | 69 ++++++++++++++++ src/setuptools_scm/_types.py | 3 + src/setuptools_scm/git.py | 85 +++++++++++++++++++- testing/test_git.py | 143 +++++++++++++++++++++++++++++++++- 6 files changed, 311 insertions(+), 7 deletions(-) diff --git a/docs/config.md b/docs/config.md index 0f80ca94..bf42099c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -97,6 +97,20 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ Defaults to the value set by [setuptools_scm.git.DEFAULT_DESCRIBE][] +`scm.git.pre_parse` +: A string specifying which git pre-parse function to use before parsing version information. + Available options: + + - `"warn_on_shallow"` (default): Warns when the repository is shallow + - `"fail_on_shallow"`: Fails with an error when the repository is shallow + - `"fetch_on_shallow"`: Automatically fetches to rectify shallow repositories + - `"fail_on_missing_submodules"`: Fails when submodules are defined but not initialized + + The `"fail_on_missing_submodules"` option is useful to prevent packaging incomplete + projects when submodules are required for a complete build. + + Note: This setting is overridden by any explicit `pre_parse` parameter passed to the git parse function. + `normalize` : A boolean flag indicating if the version string should be normalized. Defaults to `True`. Setting this to `False` is equivalent to setting diff --git a/docs/usage.md b/docs/usage.md index faede38c..ee7c168e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -39,6 +39,10 @@ dynamic = ["version"] [tool.setuptools_scm] # Configure custom options here (version schemes, file writing, etc.) version_file = "src/mypackage/_version.py" + +# Example: Fail if submodules are not initialized (useful for projects requiring submodules) +[tool.setuptools_scm.scm.git] +pre_parse = "fail_on_missing_submodules" ``` Both approaches will work with projects that support PEP 518 ([pip](https://pypi.org/project/pip) and diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index aec2a0f0..210169c6 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -8,10 +8,14 @@ import warnings from pathlib import Path +from typing import TYPE_CHECKING from typing import Any from typing import Pattern from typing import Protocol +if TYPE_CHECKING: + from . import git + from . import _log from . import _types as _t from ._integration.pyproject_reading import PyProjectData @@ -52,6 +56,13 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]: return regex +def _get_default_git_pre_parse() -> git.GitPreParse: + """Get the default git pre_parse enum value""" + from . import git + + return git.GitPreParse.WARN_ON_SHALLOW + + class ParseFunction(Protocol): def __call__( self, root: _t.PathT, *, config: Configuration @@ -83,6 +94,53 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str: return os.path.abspath(root) +@dataclasses.dataclass +class GitConfiguration: + """Git-specific configuration options""" + + pre_parse: git.GitPreParse = dataclasses.field( + default_factory=lambda: _get_default_git_pre_parse() + ) + + @classmethod + def from_data(cls, data: dict[str, Any]) -> GitConfiguration: + """Create GitConfiguration from configuration data, converting strings to enums""" + git_data = data.copy() + + # Convert string pre_parse values to enum instances + if "pre_parse" in git_data and isinstance(git_data["pre_parse"], str): + from . import git + + try: + git_data["pre_parse"] = git.GitPreParse(git_data["pre_parse"]) + except ValueError as e: + valid_options = [option.value for option in git.GitPreParse] + raise ValueError( + f"Invalid git pre_parse function '{git_data['pre_parse']}'. " + f"Valid options are: {', '.join(valid_options)}" + ) from e + + return cls(**git_data) + + +@dataclasses.dataclass +class ScmConfiguration: + """SCM-specific configuration options""" + + git: GitConfiguration = dataclasses.field(default_factory=GitConfiguration) + + @classmethod + def from_data(cls, data: dict[str, Any]) -> ScmConfiguration: + """Create ScmConfiguration from configuration data""" + scm_data = data.copy() + + # Handle git-specific configuration + git_data = scm_data.pop("git", {}) + git_config = GitConfiguration.from_data(git_data) + + return cls(git=git_config, **scm_data) + + @dataclasses.dataclass class Configuration: """Global configuration model""" @@ -107,6 +165,11 @@ class Configuration: parent: _t.PathT | None = None + # Nested SCM configurations + scm: ScmConfiguration = dataclasses.field( + default_factory=lambda: ScmConfiguration() + ) + def __post_init__(self) -> None: self.tag_regex = _check_tag_regex(self.tag_regex) @@ -161,8 +224,14 @@ def from_data( version_cls = _validate_version_cls( data.pop("version_cls", None), data.pop("normalize", True) ) + + # Handle nested SCM configuration + scm_data = data.pop("scm", {}) + scm_config = ScmConfiguration.from_data(scm_data) + return cls( relative_to=relative_to, version_cls=version_cls, + scm=scm_config, **data, ) diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index b655c76f..6cc4e774 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -26,3 +26,6 @@ VERSION_SCHEME: TypeAlias = Union[str, Callable[["version.ScmVersion"], str]] VERSION_SCHEMES: TypeAlias = Union[List[str], Tuple[str, ...], VERSION_SCHEME] SCMVERSION: TypeAlias = "version.ScmVersion" + +# Git pre-parse function types +GIT_PRE_PARSE: TypeAlias = Union[str, None] diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 7d81d40c..f778e96f 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -11,6 +11,7 @@ from datetime import date from datetime import datetime from datetime import timezone +from enum import Enum from os.path import samefile from pathlib import Path from typing import TYPE_CHECKING @@ -52,6 +53,15 @@ ] +class GitPreParse(Enum): + """Available git pre-parse functions""" + + WARN_ON_SHALLOW = "warn_on_shallow" + FAIL_ON_SHALLOW = "fail_on_shallow" + FETCH_ON_SHALLOW = "fetch_on_shallow" + FAIL_ON_MISSING_SUBMODULES = "fail_on_missing_submodules" + + def run_git( args: Sequence[str | os.PathLike[str]], repo: Path, @@ -209,6 +219,65 @@ def fail_on_shallow(wd: GitWorkdir) -> None: ) +def fail_on_missing_submodules(wd: GitWorkdir) -> None: + """ + Fail if submodules are defined but not initialized/cloned. + + This pre_parse function checks if there are submodules defined in .gitmodules + but not properly initialized (cloned). This helps prevent packaging incomplete + projects when submodules are required for a complete build. + """ + gitmodules_path = wd.path / ".gitmodules" + if not gitmodules_path.exists(): + # No submodules defined, nothing to check + return + + # Get submodule status - lines starting with '-' indicate uninitialized submodules + status_result = run_git(["submodule", "status"], wd.path) + if status_result.returncode != 0: + # Command failed, might not be in a git repo or other error + log.debug("Failed to check submodule status: %s", status_result.stderr) + return + + status_lines = ( + status_result.stdout.strip().split("\n") if status_result.stdout.strip() else [] + ) + uninitialized_submodules = [] + + for line in status_lines: + line = line.strip() + if line.startswith("-"): + # Extract submodule path (everything after the commit hash) + parts = line.split() + if len(parts) >= 2: + submodule_path = parts[1] + uninitialized_submodules.append(submodule_path) + + # If .gitmodules exists but git submodule status returns nothing, + # it means submodules are defined but not properly set up (common after cloning without --recurse-submodules) + if not status_lines and gitmodules_path.exists(): + raise ValueError( + f"Submodules are defined in .gitmodules but not initialized in {wd.path}. " + f"Please run 'git submodule update --init --recursive' to initialize them." + ) + + if uninitialized_submodules: + submodule_list = ", ".join(uninitialized_submodules) + raise ValueError( + f"Submodules are not initialized in {wd.path}: {submodule_list}. " + f"Please run 'git submodule update --init --recursive' to initialize them." + ) + + +# Mapping from enum items to actual pre_parse functions +_GIT_PRE_PARSE_FUNCTIONS: dict[GitPreParse, Callable[[GitWorkdir], None]] = { + GitPreParse.WARN_ON_SHALLOW: warn_on_shallow, + GitPreParse.FAIL_ON_SHALLOW: fail_on_shallow, + GitPreParse.FETCH_ON_SHALLOW: fetch_on_shallow, + GitPreParse.FAIL_ON_MISSING_SUBMODULES: fail_on_missing_submodules, +} + + def get_working_directory(config: Configuration, root: _t.PathT) -> GitWorkdir | None: """ Return the working directory (``GitWorkdir``). @@ -231,16 +300,26 @@ def parse( root: _t.PathT, config: Configuration, describe_command: str | list[str] | None = None, - pre_parse: Callable[[GitWorkdir], None] = warn_on_shallow, + pre_parse: Callable[[GitWorkdir], None] | None = None, ) -> ScmVersion | None: """ - :param pre_parse: experimental pre_parse action, may change at any time + :param pre_parse: experimental pre_parse action, may change at any time. + Takes precedence over config.git_pre_parse if provided. """ _require_command("git") wd = get_working_directory(config, root) if wd: + # Use function parameter first, then config setting, then default + if pre_parse is not None: + effective_pre_parse = pre_parse + else: + # config.scm.git.pre_parse is always a GitPreParse enum instance + effective_pre_parse = _GIT_PRE_PARSE_FUNCTIONS.get( + config.scm.git.pre_parse, warn_on_shallow + ) + return _git_parse_inner( - config, wd, describe_command=describe_command, pre_parse=pre_parse + config, wd, describe_command=describe_command, pre_parse=effective_pre_parse ) else: return None diff --git a/testing/test_git.py b/testing/test_git.py index 9ef1f14a..e739fc94 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -39,15 +39,22 @@ ) -@pytest.fixture(name="wd") -def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir: - debug_mode.disable() - monkeypatch.delenv("HOME", raising=False) +def setup_git_wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch | None = None) -> WorkDir: + """Set up a WorkDir with git initialized and configured for testing.""" + if monkeypatch: + monkeypatch.delenv("HOME", raising=False) wd("git init") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" + return wd + + +@pytest.fixture(name="wd") +def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir: + debug_mode.disable() + setup_git_wd(wd, monkeypatch) debug_mode.enable() return wd @@ -623,3 +630,131 @@ def test_git_archival_from_unfiltered() -> None: ): version = archival_to_version({"node": "$Format:%H$"}, config=config) assert version is None + + +def test_fail_on_missing_submodules_no_gitmodules(wd: WorkDir) -> None: + """Test that fail_on_missing_submodules does nothing when no .gitmodules exists.""" + wd.commit_testfile() + # Should not raise any exception + git.fail_on_missing_submodules(git.GitWorkdir(wd.cwd)) + + +def test_fail_on_missing_submodules_with_initialized_submodules(wd: WorkDir) -> None: + """Test that fail_on_missing_submodules passes when submodules are initialized.""" + # Create a submodule directory and .gitmodules file + submodule_dir = wd.cwd / "external" + submodule_dir.mkdir() + + # Initialize a git repo in the submodule directory + wd(["git", "-C", str(submodule_dir), "init"]) + wd(["git", "-C", str(submodule_dir), "config", "user.email", "test@example.com"]) + wd(["git", "-C", str(submodule_dir), "config", "user.name", "Test User"]) + + # Create a commit in the submodule + test_file = submodule_dir / "test.txt" + test_file.write_text("test content") + wd(["git", "-C", str(submodule_dir), "add", "test.txt"]) + wd(["git", "-C", str(submodule_dir), "commit", "-m", "Initial commit"]) + + # Add it as a submodule to the main repo + wd(["git", "submodule", "add", str(submodule_dir), "external"]) + wd.commit_testfile() + + # Should not raise any exception since the submodule is initialized + git.fail_on_missing_submodules(git.GitWorkdir(wd.cwd)) + + +def test_fail_on_missing_submodules_with_uninitialized_submodules( + tmp_path: Path, +) -> None: + """Test that fail_on_missing_submodules fails when submodules are not initialized.""" + # Create a test repository with a .gitmodules file but no actual submodule + test_repo = tmp_path / "test_repo" + test_repo.mkdir() + test_wd = setup_git_wd(WorkDir(test_repo)) + + # Create a fake .gitmodules file (this simulates what happens after cloning without --recurse-submodules) + gitmodules_content = """[submodule "external"] + path = external + url = https://example.com/external.git +""" + test_wd.write(".gitmodules", gitmodules_content) + test_wd.add_and_commit("Add-submodule-reference") + + # Should raise ValueError for uninitialized submodules + with pytest.raises( + ValueError, match=r"Submodules are defined in \.gitmodules but not initialized" + ): + git.fail_on_missing_submodules(git.GitWorkdir(test_repo)) + + +def test_git_pre_parse_config_integration(wd: WorkDir) -> None: + """Test that git_pre_parse configuration is used by the parse function.""" + wd.commit_testfile() + + # Test with default (None) - should use warn_on_shallow + config = Configuration() + result = git.parse(str(wd.cwd), config) + assert result is not None + + # Test with explicit configuration + from setuptools_scm._config import GitConfiguration + from setuptools_scm._config import ScmConfiguration + + config_with_pre_parse = Configuration( + scm=ScmConfiguration( + git=GitConfiguration(pre_parse=git.GitPreParse.WARN_ON_SHALLOW) + ) + ) + result = git.parse(str(wd.cwd), config_with_pre_parse) + assert result is not None + + # Test with different pre_parse value + config_fail_shallow = Configuration( + scm=ScmConfiguration( + git=GitConfiguration(pre_parse=git.GitPreParse.FAIL_ON_MISSING_SUBMODULES) + ) + ) + result = git.parse(str(wd.cwd), config_fail_shallow) + assert result is not None + + +def test_nested_scm_git_config_from_toml(tmp_path: Path) -> None: + """Test that nested SCM git configuration is properly parsed from TOML.""" + # Create a test pyproject.toml with nested SCM configuration + pyproject_path = tmp_path / "pyproject.toml" + pyproject_content = """ +[tool.setuptools_scm.scm.git] +pre_parse = "fail_on_missing_submodules" +""" + pyproject_path.write_text(pyproject_content) + + # Parse the configuration from file + config = Configuration.from_file(pyproject_path) + + # Verify the nested configuration was parsed correctly and converted to enum + assert config.scm.git.pre_parse == git.GitPreParse.FAIL_ON_MISSING_SUBMODULES + + +def test_nested_scm_git_config_from_data() -> None: + """Test that nested SCM git configuration parsing works correctly with from_data.""" + # Test configuration parsing directly without file I/O + config_data = {"scm": {"git": {"pre_parse": "fail_on_missing_submodules"}}} + + # Parse the configuration data + config = Configuration.from_data(relative_to=".", data=config_data) + + # Verify the nested configuration was parsed correctly and converted to enum + assert config.scm.git.pre_parse == git.GitPreParse.FAIL_ON_MISSING_SUBMODULES + + +def test_invalid_git_pre_parse_raises_error() -> None: + """Test that invalid git pre_parse values raise a helpful ValueError.""" + # Test configuration parsing directly without file I/O + invalid_config_data = {"scm": {"git": {"pre_parse": "invalid_function"}}} + + # Parse the configuration data - should raise ValueError + with pytest.raises( + ValueError, match="Invalid git pre_parse function 'invalid_function'" + ): + Configuration.from_data(relative_to=".", data=invalid_config_data) From b69d0aef9e16edc57c98d9da733928e314b88513 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:17:39 +0000 Subject: [PATCH 047/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.4 → v0.12.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.4...v0.12.5) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c840b357..61fe40d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.4 + rev: v0.12.5 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix, --show-fixes] From ca11490b35132a28bfba6d3d8e8c3f3905ffa436 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 28 Jul 2025 21:56:33 +0200 Subject: [PATCH 048/162] Deprecate `git_describe_command` in favor of `scm.git.describe_command` - implement backward compatibility handling - Update documentation and tests to reflect changes --- docs/config.md | 9 ++- docs/usage.md | 5 +- src/setuptools_scm/_config.py | 95 ++++++++++++++++++++++++- src/setuptools_scm/_get_version_impl.py | 15 +++- src/setuptools_scm/git.py | 4 +- testing/test_git.py | 67 ++++++++++++++++- 6 files changed, 185 insertions(+), 10 deletions(-) diff --git a/docs/config.md b/docs/config.md index bf42099c..c8b4d9ff 100644 --- a/docs/config.md +++ b/docs/config.md @@ -92,7 +92,7 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ this is a function for advanced use and you should be familiar with the `setuptools-scm` internals to use it. -`git_describe_command` +`scm.git.describe_command` : This command will be used instead the default `git describe --long` command. Defaults to the value set by [setuptools_scm.git.DEFAULT_DESCRIBE][] @@ -106,11 +106,16 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ - `"fetch_on_shallow"`: Automatically fetches to rectify shallow repositories - `"fail_on_missing_submodules"`: Fails when submodules are defined but not initialized - The `"fail_on_missing_submodules"` option is useful to prevent packaging incomplete + The `"fail_on_missing_submodules"` option is useful to prevent packaging incomplete projects when submodules are required for a complete build. Note: This setting is overridden by any explicit `pre_parse` parameter passed to the git parse function. +`git_describe_command` (deprecated) +: **Deprecated since 8.4.0**: Use `scm.git.describe_command` instead. + + This field is maintained for backward compatibility but will issue a deprecation warning when used. + `normalize` : A boolean flag indicating if the version string should be normalized. Defaults to `True`. Setting this to `False` is equivalent to setting diff --git a/docs/usage.md b/docs/usage.md index ee7c168e..0579163e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -40,9 +40,10 @@ dynamic = ["version"] # Configure custom options here (version schemes, file writing, etc.) version_file = "src/mypackage/_version.py" -# Example: Fail if submodules are not initialized (useful for projects requiring submodules) +# Example: Git-specific configuration [tool.setuptools_scm.scm.git] -pre_parse = "fail_on_missing_submodules" +pre_parse = "fail_on_missing_submodules" # Fail if submodules are not initialized +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 diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 210169c6..0387de9d 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -30,6 +30,57 @@ log = _log.log.getChild("config") + +def _is_called_from_dataclasses() -> bool: + """Check if the current call is from the dataclasses module.""" + import inspect + + frame = inspect.currentframe() + try: + # Walk up to 7 frames to check for dataclasses calls + current_frame = frame + assert current_frame is not None + for _ in range(7): + current_frame = current_frame.f_back + if current_frame is None: + break + if "dataclasses.py" in current_frame.f_code.co_filename: + return True + return False + finally: + del frame + + +class _GitDescribeCommandDescriptor: + """Data descriptor for deprecated git_describe_command field.""" + + def __get__( + self, obj: Configuration | None, objtype: type[Configuration] | None = None + ) -> _t.CMD_TYPE | None: + if obj is None: + return self # type: ignore[return-value] + + # Only warn if not being called by dataclasses.replace or similar introspection + is_from_dataclasses = _is_called_from_dataclasses() + if not is_from_dataclasses: + warnings.warn( + "Configuration field 'git_describe_command' is deprecated. " + "Use 'scm.git.describe_command' instead.", + DeprecationWarning, + stacklevel=2, + ) + return obj.scm.git.describe_command + + def __set__(self, obj: Configuration, value: _t.CMD_TYPE | None) -> None: + warnings.warn( + "Configuration field 'git_describe_command' is deprecated. " + "Use 'scm.git.describe_command' instead.", + DeprecationWarning, + stacklevel=2, + ) + obj.scm.git.describe_command = value + + DEFAULT_TAG_REGEX = re.compile( r"^(?:[\w-]+-)?(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$" ) @@ -101,6 +152,7 @@ class GitConfiguration: pre_parse: git.GitPreParse = dataclasses.field( default_factory=lambda: _get_default_git_pre_parse() ) + describe_command: _t.CMD_TYPE | None = None @classmethod def from_data(cls, data: dict[str, Any]) -> GitConfiguration: @@ -158,7 +210,10 @@ class Configuration: version_file: _t.PathT | None = None version_file_template: str | None = None parse: ParseFunction | None = None - git_describe_command: _t.CMD_TYPE | None = None + git_describe_command: dataclasses.InitVar[_t.CMD_TYPE | None] = ( + _GitDescribeCommandDescriptor() + ) + dist_name: str | None = None version_cls: type[_VersionT] = _Version search_parent_directories: bool = False @@ -170,9 +225,42 @@ class Configuration: default_factory=lambda: ScmConfiguration() ) - def __post_init__(self) -> None: + # Deprecated fields (handled in __post_init__) + + def __post_init__(self, git_describe_command: _t.CMD_TYPE | None) -> None: self.tag_regex = _check_tag_regex(self.tag_regex) + # Handle deprecated git_describe_command + # Check if it's a descriptor object (happens when no value is passed) + if git_describe_command is not None and not isinstance( + git_describe_command, _GitDescribeCommandDescriptor + ): + # Check if this is being called from dataclasses + is_from_dataclasses = _is_called_from_dataclasses() + + same_value = ( + self.scm.git.describe_command is not None + and self.scm.git.describe_command == git_describe_command + ) + + if is_from_dataclasses and same_value: + # Ignore the passed value - it's from dataclasses.replace() with same value + pass + else: + warnings.warn( + "Configuration field 'git_describe_command' is deprecated. " + "Use 'scm.git.describe_command' instead.", + DeprecationWarning, + stacklevel=2, + ) + # Check for conflicts + if self.scm.git.describe_command is not None: + raise ValueError( + "Cannot specify both 'git_describe_command' (deprecated) and " + "'scm.git.describe_command'. Please use only 'scm.git.describe_command'." + ) + self.scm.git.describe_command = git_describe_command + @property def absolute_root(self) -> str: return _check_absolute_root(self.root, self.relative_to) @@ -227,6 +315,9 @@ def from_data( # Handle nested SCM configuration scm_data = data.pop("scm", {}) + + # Handle nested SCM configuration + scm_config = ScmConfiguration.from_data(scm_data) return cls( diff --git a/src/setuptools_scm/_get_version_impl.py b/src/setuptools_scm/_get_version_impl.py index 40217cc6..ccbeac3b 100644 --- a/src/setuptools_scm/_get_version_impl.py +++ b/src/setuptools_scm/_get_version_impl.py @@ -154,6 +154,7 @@ def get_version( version_cls: Any | None = None, normalize: bool = True, search_parent_directories: bool = False, + scm: dict[str, Any] | None = None, ) -> str: """ If supplied, relative_to should be a file from which root may @@ -165,7 +166,19 @@ def get_version( version_cls = _validate_version_cls(version_cls, normalize) del normalize tag_regex = parse_tag_regex(tag_regex) - config = Configuration(**locals()) + + # Handle scm parameter by converting it to ScmConfiguration + if scm is not None: + scm_config = _config.ScmConfiguration.from_data(scm) + else: + scm_config = _config.ScmConfiguration() + + # Remove scm from locals() since we handle it separately + config_params = locals().copy() + config_params.pop("scm", None) + config_params.pop("scm_config", None) + + config = _config.Configuration(scm=scm_config, **config_params) maybe_version = _get_version(config, force_write_version_files=True) if maybe_version is None: diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index f778e96f..043d8ad4 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -330,8 +330,8 @@ def version_from_describe( config: Configuration, describe_command: _t.CMD_TYPE | None, ) -> ScmVersion | None: - if config.git_describe_command is not None: - describe_command = config.git_describe_command + if config.scm.git.describe_command is not None: + describe_command = config.scm.git.describe_command if describe_command is not None: if isinstance(describe_command, str): diff --git a/testing/test_git.py b/testing/test_git.py index e739fc94..c10fe476 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -435,7 +435,11 @@ def test_not_matching_tags(wd: WorkDir) -> None: wd.commit_testfile() assert wd.get_version( tag_regex=r"^apache-arrow-([\.0-9]+)$", - git_describe_command="git describe --dirty --tags --long --exclude *js* ", + scm={ + "git": { + "describe_command": "git describe --dirty --tags --long --exclude *js* " + } + }, ).startswith("0.11.2") @@ -758,3 +762,64 @@ def test_invalid_git_pre_parse_raises_error() -> None: ValueError, match="Invalid git pre_parse function 'invalid_function'" ): Configuration.from_data(relative_to=".", data=invalid_config_data) + + +def test_git_describe_command_backward_compatibility() -> None: + """Test backward compatibility for git_describe_command configuration.""" + # Test old configuration style still works with deprecation warning + old_config_data = { + "git_describe_command": "git describe --dirty --tags --long --exclude *js*" + } + + with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"): + config = Configuration.from_data(relative_to=".", data=old_config_data) + + # Verify it was migrated to the new location + assert ( + config.scm.git.describe_command + == "git describe --dirty --tags --long --exclude *js*" + ) + + +def test_git_describe_command_from_data_conflict() -> None: + """Test that specifying both old and new configuration in from_data raises ValueError.""" + # Both old and new configuration specified - should raise ValueError + mixed_config_data = { + "git_describe_command": "old command", + "scm": {"git": {"describe_command": "new command"}}, + } + + # The Configuration constructor should handle the conflict detection + with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"): + with pytest.raises( + ValueError, match=r"Cannot specify both.*git_describe_command" + ): + Configuration.from_data(relative_to=".", data=mixed_config_data) + + +def test_git_describe_command_init_argument_deprecation() -> None: + """Test that passing git_describe_command as init argument issues deprecation warning.""" + # Test init argument + with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"): + config = Configuration(git_describe_command="test command") + + # Verify the value was migrated to the new location + assert config.scm.git.describe_command == "test command" + + +def test_git_describe_command_init_conflict() -> None: + """Test that specifying both old and new configuration raises ValueError.""" + from setuptools_scm._config import GitConfiguration + from setuptools_scm._config import ScmConfiguration + + # Both old init arg and new configuration specified - should raise ValueError + with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"): + with pytest.raises( + ValueError, match=r"Cannot specify both.*git_describe_command" + ): + Configuration( + git_describe_command="old command", + scm=ScmConfiguration( + git=GitConfiguration(describe_command="new command") + ), + ) From c94d79711cdfd85ac05cb1f5d0b5e4edeb952c0b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 28 Jul 2025 22:11:16 +0200 Subject: [PATCH 049/162] changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c16454c8..90d24811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - 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 +- add `scm` parameter support to `get_version()` function for nested SCM configuration ### Changed - add `pip` to test optional dependencies for improved uv venv compatibility @@ -27,7 +28,9 @@ - fix #1136: update customizing.md to fix missing import - fix #1001: document the missing version schemes and add examples in the docs - fix #1115: explicitly document file finder behaviour -- fix #879: add test that validates caswe differenct behavior +- fix #879: add test that validates case different behavior on windows +- migrate git describe command to new scm config +- add support for failing on missing submodules ## v8.3.1 From 69fb3a5a393948fbf00d68fd99d301dc0f5aae7c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 29 Jul 2025 10:24:06 +0200 Subject: [PATCH 050/162] fix #279: expand erros when relative to could/should be used --- CHANGELOG.md | 1 + src/setuptools_scm/_get_version_impl.py | 75 +++++++++++++++++++++---- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d24811..ce25d3f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - fix #879: add test that validates case different behavior on windows - migrate git describe command to new scm config - add support for failing on missing submodules +- fix #279: expand errors when scm can be found upwards and relative_to wasnt used ## v8.3.1 diff --git a/src/setuptools_scm/_get_version_impl.py b/src/setuptools_scm/_get_version_impl.py index ccbeac3b..31bc9c39 100644 --- a/src/setuptools_scm/_get_version_impl.py +++ b/src/setuptools_scm/_get_version_impl.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import logging import re import warnings @@ -119,21 +120,73 @@ def _get_version( return version_string +def _find_scm_in_parents(config: Configuration) -> Path | None: + """ + Search parent directories for SCM repositories when relative_to is not set. + Uses the existing entrypoint system for SCM discovery. + """ + if config.search_parent_directories: + return None + + searching_config = dataclasses.replace(config, search_parent_directories=True) + + from .discover import iter_matching_entrypoints + + for _ep in iter_matching_entrypoints( + config.absolute_root, "setuptools_scm.parse_scm", searching_config + ): + # xxx: iter_matching_entrypoints should return the parent directory, we do a hack atm + assert searching_config.parent is not None + return Path(searching_config.parent) + + return None + + def _version_missing(config: Configuration) -> NoReturn: - raise LookupError( + base_error = ( f"setuptools-scm was unable to detect version for {config.absolute_root}.\n\n" - "Make sure you're either building from a fully intact git repository " - "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a " - "git checkout without the .git folder) don't contain the necessary " - "metadata and will not work.\n\n" - "For example, if you're using pip, instead of " - "https://github.com/user/proj/archive/master.zip " - "use git+https://github.com/user/proj.git#egg=proj\n\n" - "Alternatively, set the version with the environment variable " - "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME} as described " - "in https://setuptools-scm.readthedocs.io/en/latest/config." ) + # If relative_to is not set, check for SCM repositories in parent directories + scm_parent = None + if config.relative_to is None: + scm_parent = _find_scm_in_parents(config) + + if scm_parent is not None: + # Found an SCM repository in a parent directory + error_msg = ( + base_error + + f"However, a repository was found in a parent directory: {scm_parent}\n\n" + f"To fix this, you have a few options:\n\n" + f"1. Use the 'relative_to' parameter to specify the file that setuptools-scm should use as reference:\n" + f" setuptools_scm.get_version(relative_to=__file__)\n\n" + f"2. Enable parent directory search in your configuration:\n" + f" [tool.setuptools_scm]\n" + f" search_parent_directories = true\n\n" + f"3. Change your working directory to the repository root: {scm_parent}\n\n" + f"4. Set the root explicitly in your configuration:\n" + f" [tool.setuptools_scm]\n" + f' root = "{scm_parent}"\n\n' + "For more information, see: https://setuptools-scm.readthedocs.io/en/latest/config/" + ) + else: + # No SCM repository found in parent directories either + error_msg = ( + base_error + + "Make sure you're either building from a fully intact git repository " + "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a " + "git checkout without the .git folder) don't contain the necessary " + "metadata and will not work.\n\n" + "For example, if you're using pip, instead of " + "https://github.com/user/proj/archive/master.zip " + "use git+https://github.com/user/proj.git#egg=proj\n\n" + "Alternatively, set the version with the environment variable " + "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME} as described " + "in https://setuptools-scm.readthedocs.io/en/latest/config/" + ) + + raise LookupError(error_msg) + def get_version( root: _t.PathT = ".", From e804af2ecdac64e40630bb19ad608df8b2673114 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 29 Jul 2025 13:33:18 +0200 Subject: [PATCH 051/162] fix #577: intoduce explicit short node --- CHANGELOG.md | 1 + .../_integration/dump_version.py | 23 ++----------------- src/setuptools_scm/version.py | 5 ++++ testing/test_basic_api.py | 2 +- testing/test_integration.py | 4 ++-- 5 files changed, 11 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce25d3f1..72e655de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - migrate git describe command to new scm config - add support for failing on missing submodules - fix #279: expand errors when scm can be found upwards and relative_to wasnt used +- fix #577: introduce explicit scmversion node and short node ## v8.3.1 diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py index 6918bd19..62827a1b 100644 --- a/src/setuptools_scm/_integration/dump_version.py +++ b/src/setuptools_scm/_integration/dump_version.py @@ -6,29 +6,12 @@ from .. import _types as _t from .._log import log as parent_log -from .._node_utils import _format_node_for_output from .._version_cls import _version_as_tuple from ..version import ScmVersion log = parent_log.getChild("dump_version") -class _TemplateScmVersion: - """Wrapper for ScmVersion that formats node for template output.""" - - def __init__(self, scm_version: ScmVersion) -> None: - self._scm_version = scm_version - - def __getattr__(self, name: str) -> object: - # Delegate all attribute access to the wrapped ScmVersion - return getattr(self._scm_version, name) - - @property - def node(self) -> str | None: - """Return the node formatted for output.""" - return _format_node_for_output(self._scm_version.node) - - TEMPLATES = { ".py": """\ # file generated by setuptools-scm @@ -64,7 +47,7 @@ def node(self) -> str | None: __version__ = version = {version!r} __version_tuple__ = version_tuple = {version_tuple!r} -__commit_id__ = commit_id = {scm_version.node!r} +__commit_id__ = commit_id = {scm_version.short_node!r} """, ".txt": "{version}", } @@ -119,12 +102,10 @@ def write_version_to_path( log.debug("dump %s into %s", version, target) version_tuple = _version_as_tuple(version) if scm_version is not None: - # Wrap ScmVersion to provide formatted node for templates - template_scm_version = _TemplateScmVersion(scm_version) content = final_template.format( version=version, version_tuple=version_tuple, - scm_version=template_scm_version, + scm_version=scm_version, ) else: content = final_template.format(version=version, version_tuple=version_tuple) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index c3bc1148..69b184eb 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -159,6 +159,11 @@ def exact(self) -> bool: """returns true checked out exactly on a tag and no local changes apply""" return self.distance == 0 and not self.dirty + @property + def short_node(self) -> str | None: + """Return the node formatted for output.""" + return _format_node_for_output(self.node) + def __repr__(self) -> str: return ( f" Date: Tue, 29 Jul 2025 15:54:12 +0200 Subject: [PATCH 052/162] add missed tests --- testing/test_better_root_errors.py | 203 +++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 testing/test_better_root_errors.py diff --git a/testing/test_better_root_errors.py b/testing/test_better_root_errors.py new file mode 100644 index 00000000..16985ecc --- /dev/null +++ b/testing/test_better_root_errors.py @@ -0,0 +1,203 @@ +""" +Tests for better error messages when relative_to is not set. + +This addresses the issue #279 where error messages should be more helpful +when setuptools-scm fails to detect a version but a repository exists +in a parent directory. +""" + +from __future__ import annotations + +import pytest + +from setuptools_scm import Configuration +from setuptools_scm import get_version +from setuptools_scm._get_version_impl import _find_scm_in_parents +from setuptools_scm._get_version_impl import _version_missing +from testing.wd_wrapper import WorkDir + + +def setup_git_repo(wd: WorkDir) -> WorkDir: + """Set up a git repository for testing.""" + wd("git init") + wd("git config user.email test@example.com") + wd('git config user.name "a test"') + wd.add_command = "git add ." + wd.commit_command = "git commit -m test-{reason}" + return wd + + +def setup_hg_repo(wd: WorkDir) -> WorkDir: + """Set up a mercurial repository for testing.""" + try: + wd("hg init") + wd("hg config --local ui.username 'test '") + wd.add_command = "hg add ." + wd.commit_command = "hg commit -m test-{reason}" + return wd + except Exception: + pytest.skip("hg not available") + + +def test_find_scm_in_parents_finds_git(wd: WorkDir) -> None: + """Test that _find_scm_in_parents correctly finds git repositories in parent directories.""" + # Set up git repo in root + setup_git_repo(wd) + + # Create a subdirectory structure + subdir = wd.cwd / "subproject" / "nested" + subdir.mkdir(parents=True) + + # Test from the nested subdirectory + config = Configuration(root=str(subdir)) + found_scm = _find_scm_in_parents(config) + + assert found_scm == wd.cwd + + +def test_find_scm_in_parents_finds_hg(wd: WorkDir) -> None: + """Test that _find_scm_in_parents correctly finds mercurial repositories in parent directories.""" + # Set up hg repo in root + setup_hg_repo(wd) + + # Create a subdirectory structure + subdir = wd.cwd / "subproject" / "nested" + subdir.mkdir(parents=True) + + # Test from the nested subdirectory + config = Configuration(root=str(subdir)) + found_scm = _find_scm_in_parents(config) + + assert found_scm == wd.cwd + + +def test_find_scm_in_parents_returns_none(wd: WorkDir) -> None: + """Test that _find_scm_in_parents returns None when no SCM repository is found.""" + # Don't initialize any SCM, just create subdirectories + subdir = wd.cwd / "project" / "nested" + subdir.mkdir(parents=True) + + config = Configuration(root=str(subdir)) + found_scm = _find_scm_in_parents(config) + + assert found_scm is None + + +def test_version_missing_with_scm_in_parent(wd: WorkDir) -> None: + """Test that _version_missing provides helpful error message when SCM is found in parent.""" + # Set up git repo in root + setup_git_repo(wd) + + # Create a subdirectory structure + subdir = wd.cwd / "subproject" / "nested" + subdir.mkdir(parents=True) + + # Test error message when relative_to is not set + config = Configuration(root=str(subdir), relative_to=None) + + with pytest.raises(LookupError) as exc_info: + _version_missing(config) + + error_message = str(exc_info.value) + + # Check that the error message mentions the parent repository + assert f"repository was found in a parent directory: {wd.cwd}" in error_message + assert "relative_to" in error_message + assert "search_parent_directories = true" in error_message + assert "setuptools_scm.get_version(relative_to=__file__)" in error_message + + +def test_version_missing_no_scm_found(wd: WorkDir) -> None: + """Test that _version_missing provides standard error message when no SCM is found anywhere.""" + # Don't initialize any SCM, just create subdirectories + subdir = wd.cwd / "project" / "nested" + subdir.mkdir(parents=True) + + config = Configuration(root=str(subdir), relative_to=None) + + with pytest.raises(LookupError) as exc_info: + _version_missing(config) + + error_message = str(exc_info.value) + + # Check that it falls back to the standard error message + assert ( + "Make sure you're either building from a fully intact git repository" + in error_message + ) + assert "repository was found in a parent directory" not in error_message + + +def test_version_missing_with_relative_to_set(wd: WorkDir) -> None: + """Test that when relative_to is set, we don't search parents for error messages.""" + # Set up git repo in root + setup_git_repo(wd) + + # Create a subdirectory structure + subdir = wd.cwd / "subproject" / "nested" + subdir.mkdir(parents=True) + + # Create a dummy file to use as relative_to + dummy_file = subdir / "setup.py" + dummy_file.write_text("# dummy file") + + # Test error message when relative_to IS set + config = Configuration(root=str(subdir), relative_to=str(dummy_file)) + + with pytest.raises(LookupError) as exc_info: + _version_missing(config) + + error_message = str(exc_info.value) + + # Should not mention parent directory when relative_to is set + assert "repository was found in a parent directory" not in error_message + assert ( + "Make sure you're either building from a fully intact git repository" + in error_message + ) + + +def test_search_parent_directories_works_as_suggested( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that the suggested search_parent_directories=True solution actually works.""" + # Set up git repo + setup_git_repo(wd) + wd.commit_testfile() # Make sure there's a commit for version detection + + # Create a subdirectory + subdir = wd.cwd / "subproject" + subdir.mkdir() + + # Change to the subdirectory + monkeypatch.chdir(subdir) + + # This should work with search_parent_directories=True + version = get_version(search_parent_directories=True) + assert version is not None + assert "0.1.dev" in version + + +def test_integration_better_error_from_nested_directory( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Integration test: get_version from nested directory should give helpful error.""" + # Set up git repo + setup_git_repo(wd) + + # Create a subdirectory + subdir = wd.cwd / "subproject" + subdir.mkdir() + + # Change to the subdirectory + monkeypatch.chdir(subdir) + + # Try to get version without any configuration + with pytest.raises(LookupError) as exc_info: + get_version() + + error_message = str(exc_info.value) + + # Should suggest helpful solutions + assert f"repository was found in a parent directory: {wd.cwd}" in error_message + assert "search_parent_directories = true" in error_message From cc3a61e1ea40e860d01171c2585e5b3794fa587c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 29 Jul 2025 21:55:40 +0200 Subject: [PATCH 053/162] fix #1100: document readthedocs workaround for dirty worktrees --- CHANGELOG.md | 1 + docs/integrations.md | 26 ++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 28 insertions(+) create mode 100644 docs/integrations.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e655de..f5360893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - add support for failing on missing submodules - fix #279: expand errors when scm can be found upwards and relative_to wasnt used - fix #577: introduce explicit scmversion node and short node +- fix #1100: add workaround for readthedocs worktress to the docs ## v8.3.1 diff --git a/docs/integrations.md b/docs/integrations.md new file mode 100644 index 00000000..09828b04 --- /dev/null +++ b/docs/integrations.md @@ -0,0 +1,26 @@ +# Integrations + +## ReadTheDocs + +### Avoid having a dirty Git index + +When building documentation on ReadTheDocs, file changes during the build process can cause setuptools-scm to detect a "dirty" working directory. + +To avoid this issue, ReadTheDocs recommends using build customization to clean the Git state after checkout: + +```yaml title=".readthedocs.yaml" +version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3.10" + jobs: + post_checkout: + # Avoid setuptools-scm dirty Git index issues + - git reset --hard HEAD + - git clean -fdx +``` + +This ensures a clean Git working directory before setuptools-scm detects the version, preventing unwanted local version components. + +Reference: [ReadTheDocs Build Customization - Avoid having a dirty Git index](https://docs.readthedocs.com/platform/stable/build-customization.html#avoid-having-a-dirty-git-index) \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 7d0553f8..5b1f42ef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,7 @@ nav: - usage.md - customizing.md - config.md + - integrations.md - extending.md - overrides.md - changelog.md From 8d58d62f7207aaee3bb6eab6b60565be5f7ba19c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 29 Jul 2025 22:03:41 +0200 Subject: [PATCH 054/162] fix #790: document rtd workaround to ensure failure on too shallow scm --- CHANGELOG.md | 1 + docs/integrations.md | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5360893..9c5207c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - fix #279: expand errors when scm can be found upwards and relative_to wasnt used - fix #577: introduce explicit scmversion node and short node - fix #1100: add workaround for readthedocs worktress to the docs +- fix #790: document shallow fail for rtd ## v8.3.1 diff --git a/docs/integrations.md b/docs/integrations.md index 09828b04..ba097fe6 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -23,4 +23,28 @@ build: This ensures a clean Git working directory before setuptools-scm detects the version, preventing unwanted local version components. -Reference: [ReadTheDocs Build Customization - Avoid having a dirty Git index](https://docs.readthedocs.com/platform/stable/build-customization.html#avoid-having-a-dirty-git-index) \ No newline at end of file + + +Reference: [ReadTheDocs Build Customization - Avoid having a dirty Git index](https://docs.readthedocs.com/platform/stable/build-customization.html#avoid-having-a-dirty-git-index) + + +### Enforce fail on shallow repositories + +ReadTheDocs may sometimes use shallow Git clones that lack the full history needed for proper version detection. You can use setuptools-scm's environment variable override system to enforce `fail_on_shallow` when building on ReadTheDocs: + +```yaml title=".readthedocs.yaml" +version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3.10" + jobs: + post_checkout: + # Avoid setuptools-scm dirty Git index issues + - git reset --hard HEAD + - git clean -fdx + # Enforce fail_on_shallow for setuptools-scm + - export SETUPTOOLS_SCM_OVERRIDES_FOR_${READTHEDOCS_PROJECT//-/_}='{scm.git.pre_parse="fail_on_shallow"}' +``` + +This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow. \ No newline at end of file From befbdaa47f14a7f57a389f07e30c8df36aa996a5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 09:32:52 +0200 Subject: [PATCH 055/162] make griffe only report as warning --- .github/workflows/api-check.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml index 97f500dd..18b7d4ed 100644 --- a/.github/workflows/api-check.yml +++ b/.github/workflows/api-check.yml @@ -34,5 +34,20 @@ jobs: pip install -e .[test] - name: Run griffe API check + id: griffe-check run: | - griffe check setuptools_scm -ssrc -f github \ No newline at end of file + if griffe check setuptools_scm -ssrc -f github; then + echo "api_check_result=success" >> $GITHUB_OUTPUT + else + echo "api_check_result=warning" >> $GITHUB_OUTPUT + echo "API stability check detected changes but will not fail the build" >> $GITHUB_STEP_SUMMARY + fi + + - name: Report API check result + if: steps.griffe-check.outputs.api_check_result == 'warning' + uses: actions/github-script@v7 + with: + script: | + core.warning('API stability check detected breaking changes. Please review the API changes above.') + core.summary.addRaw('⚠️ API Stability Warning: Breaking changes detected in the public API') + await core.summary.write() \ No newline at end of file From 6655627c43fb1a49d3fa425666a80d3899fdcf76 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 09:50:05 +0200 Subject: [PATCH 056/162] fix #474: mention the pretend version vars on the error --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c5207c9..70515bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - fix #577: introduce explicit scmversion node and short node - fix #1100: add workaround for readthedocs worktress to the docs - fix #790: document shallow fail for rtd +- fix #474: expand version not found error message to provide clearer guidance about SETUPTOOLS_SCM_PRETEND_VERSION_FOR_* environment variables ## v8.3.1 From ea0cc9aa21bb3b480ea685b6c46120ae047d1317 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 10:13:16 +0200 Subject: [PATCH 057/162] fix #324: document/recommend the v prefix --- docs/config.md | 8 ++++ docs/extending.md | 16 ++++---- docs/index.md | 15 +++++++- docs/overrides.md | 9 +++++ docs/usage.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 9 deletions(-) diff --git a/docs/config.md b/docs/config.md index c8b4d9ff..e4a77ef0 100644 --- a/docs/config.md +++ b/docs/config.md @@ -67,6 +67,14 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ named `version`, that captures the actual version information. Defaults to the value of [setuptools_scm._config.DEFAULT_TAG_REGEX][] + which supports tags with optional "v" prefix (recommended), project prefixes, + and various version formats. + + !!! tip + + The default regex supports common tag formats like `v1.0.0`, `myproject-v1.0.0`, + and `1.0.0`. For best practices on tag naming, see + [Version Tag Formats](usage.md#version-tag-formats). `parentdir_prefix_version: str|None = None` : If the normal methods for detecting the version (SCM version, diff --git a/docs/extending.md b/docs/extending.md index 3d903173..0436b138 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -58,8 +58,8 @@ representing the version. and custom `.devN` versions will trigger an error. **Examples:** - - Tag `1.0.0` → version `1.0.1.dev0` (if dirty or distance > 0) - - Tag `1.0.0` → version `1.0.0` (if exact match) + - Tag `v1.0.0` → version `1.0.1.dev0` (if dirty or distance > 0) + - Tag `v1.0.0` → version `1.0.0` (if exact match) `calver-by-date` : Calendar versioning scheme that generates versions based on dates. @@ -69,17 +69,17 @@ representing the version. for release branches. **Examples:** - - Tag `23.01.15.0` on same day → version `23.01.15.1.devN` - - Tag `23.01.15.0` on different day (e.g., 2023-01-16) → version `23.01.16.0.devN` - - Tag `2023.01.15.0` → uses 4-digit year format for new versions + - Tag `v23.01.15.0` on same day → version `23.01.15.1.devN` + - Tag `v23.01.15.0` on different day (e.g., 2023-01-16) → version `23.01.16.0.devN` + - Tag `v2023.01.15.0` → uses 4-digit year format for new versions `no-guess-dev` : Does no next version guessing, just adds `.post1.devN`. This is the recommended replacement for the deprecated `post-release` scheme. **Examples:** - - Tag `1.0.0` → version `1.0.0.post1.devN` (if distance > 0) - - Tag `1.0.0` → version `1.0.0` (if exact match) + - Tag `v1.0.0` → version `1.0.0.post1.devN` (if distance > 0) + - Tag `v1.0.0` → version `1.0.0` (if exact match) `only-version` : Only use the version from the tag, as given. @@ -87,7 +87,7 @@ representing the version. !!! warning "This means version is no longer pseudo unique per commit" **Examples:** - - Tag `1.0.0` → version `1.0.0` (always, regardless of distance or dirty state) + - Tag `v1.0.0` → version `1.0.0` (always, regardless of distance or dirty state) `post-release (deprecated)` : Generates post release versions (adds `.postN`) diff --git a/docs/index.md b/docs/index.md index 29a420fc..b03291e3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,8 +43,21 @@ dynamic = ["version"] # more missing [tool.setuptools_scm] +´´´ -``` + +!!! tip "Recommended Tag Format" + + Use the **"v" prefix** for your version tags for best compatibility: + + ```bash + git tag v1.0.0 + git tag v1.1.0 + git tag v2.0.0-rc1 + ``` + + This is a widely adopted convention that works well with setuptools-scm and other tools. + See the [Version Tag Formats](usage.md#version-tag-formats) section for more details. ### With hatch diff --git a/docs/overrides.md b/docs/overrides.md index 942bde30..1a0f2d54 100644 --- a/docs/overrides.md +++ b/docs/overrides.md @@ -50,6 +50,15 @@ Use with a specific package: export SETUPTOOLS_SCM_PRETEND_METADATA_FOR_MY_PACKAGE='{node="g1234567", distance=2}' ``` +!!! note "Node ID Prefixes" + + Node IDs must include the appropriate SCM prefix: + + - Use `g` prefix for git repositories (e.g., `g1a2b3c4d5`) + - Use `h` prefix for mercurial repositories (e.g., `h1a2b3c4d5`) + + This ensures consistency with setuptools-scm's automatic node ID formatting. + ### Use case: CI/CD environments This is particularly useful for solving issues where version file templates need access to diff --git a/docs/usage.md b/docs/usage.md index 0579163e..d48a4252 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -253,6 +253,100 @@ For Git projects, the version relies on [git describe](https://git-scm.com/docs so you will see an additional `g` prepended to the `{revision hash}`. +## Version Tag Formats + +setuptools-scm automatically detects version information from SCM tags. The default tag regex +supports a wide variety of tag formats, with the **"v" prefix being recommended** for clarity +and consistency. + +### Recommended Tag Format + +**Use the "v" prefix for version tags:** + +```bash +git tag v1.0.0 # Recommended +git tag v2.1.3 +git tag v1.0.0-alpha1 +git tag v1.0.0-rc1 +``` + +### Supported Tag Formats + +setuptools-scm's default tag regex supports: + +- **Version prefix**: `v` or `V` (optional, but recommended) +- **Project prefix**: Optional project name followed by dashes (e.g., `myproject-v1.0.0`) +- **Version number**: Standard semantic versioning patterns +- **Pre-release suffixes**: Alpha, beta, RC versions +- **Build metadata**: Anything after `+` is ignored + +**Examples of valid tags:** +```bash +# Recommended formats (with v prefix) +v1.0.0 +v2.1.3 +v1.0.0-alpha1 +v1.0.0-beta2 +v1.0.0-rc1 +v1.2.3-dev +V1.0.0 # Capital V also works + +# Project-prefixed formats +myproject-v1.0.0 +my-lib-v2.1.0 + +# Without v prefix (supported but not recommended) +1.0.0 +2.1.3 +1.0.0-alpha1 + +# With build metadata (metadata after + is ignored) +v1.0.0+build.123 +v1.0.0+20240115 +``` + +### Why Use the "v" Prefix? + +1. **Clarity**: Makes it immediately obvious that the tag represents a version +2. **Convention**: Widely adopted standard across the software industry +3. **Git compatibility**: Works well with git's tag sorting and filtering +4. **Tool compatibility**: Many other tools expect version tags to have a "v" prefix + +### Custom Tag Patterns + +If you need different tag patterns, you can customize the tag regex: + +```toml title="pyproject.toml" +[tool.setuptools_scm] +tag_regex = "^release-(?P[0-9]+\\.[0-9]+\\.[0-9]+)$" +``` + +## Node ID Prefixes + +setuptools-scm automatically prepends identifying characters to node IDs (commit/revision hashes) +to distinguish between different SCM systems: + +- **Git repositories**: Node IDs are prefixed with `g` (e.g., `g1a2b3c4d5`) +- **Mercurial repositories**: Node IDs are prefixed with `h` (e.g., `h1a2b3c4d5`) + +This prefixing serves several purposes: + +1. **SCM identification**: Makes it clear which version control system was used +2. **Consistency**: Ensures predictable node ID format across different SCM backends +3. **Debugging**: Helps identify the source SCM when troubleshooting version issues + +The prefixes are automatically added by setuptools-scm and should be included when manually +specifying node IDs in environment variables like `SETUPTOOLS_SCM_PRETEND_METADATA`. + +**Examples:** +```bash +# Git node ID +1.0.0.dev5+g1a2b3c4d5 + +# Mercurial node ID +1.0.0.dev5+h1a2b3c4d5 +``` + !!! note According to [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers>), From e53d16d285adf9c27cdf6fbc457a7c56a61254bb Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 10:20:53 +0200 Subject: [PATCH 058/162] changelog + py.typed - fixes #501 --- CHANGELOG.md | 2 ++ src/setuptools_scm/py.typed | 0 2 files changed, 2 insertions(+) create mode 100644 src/setuptools_scm/py.typed diff --git a/CHANGELOG.md b/CHANGELOG.md index 70515bb8..dd2deacb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ - fix #1100: add workaround for readthedocs worktress to the docs - fix #790: document shallow fail for rtd - fix #474: expand version not found error message to provide clearer guidance about SETUPTOOLS_SCM_PRETEND_VERSION_FOR_* environment variables +- fix #324: document/recommend the v tag prefix +- fix #501: add py.typed ## v8.3.1 diff --git a/src/setuptools_scm/py.typed b/src/setuptools_scm/py.typed new file mode 100644 index 00000000..e69de29b From 0b0750cd2a8fe80391f605ad4b715e5d788644cd Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 10:57:42 +0200 Subject: [PATCH 059/162] fix #804: consider fallback version befor defaulting to 0.0 in git --- src/setuptools_scm/git.py | 2 +- testing/test_git.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 043d8ad4..acfd8e76 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -364,7 +364,7 @@ def _git_parse_inner( if version is None: # If 'git git_describe_command' failed, try to get the information otherwise. - tag = config.version_cls("0.0") + tag = config.version_cls(config.fallback_version or "0.0") node = wd.node() if node is None: distance = 0 diff --git a/testing/test_git.py b/testing/test_git.py index c10fe476..1c024027 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -823,3 +823,31 @@ def test_git_describe_command_init_conflict() -> None: git=GitConfiguration(describe_command="new command") ), ) + + +def test_git_no_commits_uses_fallback_version(wd: WorkDir) -> None: + """Test that when git describe fails (no commits), fallback_version is used instead of 0.0.""" + # Reinitialize as empty repo to remove any existing commits + wd("rm -rf .git") + wd("git init") + wd("git config user.email test@example.com") + wd('git config user.name "a test"') + + # Test with fallback_version set - should use the fallback instead of "0.0" + config = Configuration(fallback_version="1.2.3") + version = git.parse(str(wd.cwd), config) + + # Should get a version starting with the fallback version + assert version is not None + assert str(version.tag) == "1.2.3" + assert version.distance == 0 + assert version.dirty is True # No commits means dirty + + # Test without fallback_version - should default to "0.0" + config_no_fallback = Configuration() + version_no_fallback = git.parse(str(wd.cwd), config_no_fallback) + + assert version_no_fallback is not None + assert str(version_no_fallback.tag) == "0.0" + assert version_no_fallback.distance == 0 + assert version_no_fallback.dirty is True From 3b34ae291b3e22e637cf2da4d76b78c0241a62b2 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 15:57:11 +0200 Subject: [PATCH 060/162] drop scriv --- CHANGELOG.md | 3 ++- changelog.d/.keep | 0 hatch.toml | 6 +----- pyproject.toml | 3 --- 4 files changed, 3 insertions(+), 9 deletions(-) delete mode 100644 changelog.d/.keep diff --git a/CHANGELOG.md b/CHANGELOG.md index dd2deacb..f7713d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ - 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 - +- drop scriv ### Fixed @@ -38,6 +38,7 @@ - fix #474: expand version not found error message to provide clearer guidance about SETUPTOOLS_SCM_PRETEND_VERSION_FOR_* environment variables - fix #324: document/recommend the v tag prefix - fix #501: add py.typed +- fix #804: git - use fallback version instead of 0.0 when no version is found at all ## v8.3.1 diff --git a/changelog.d/.keep b/changelog.d/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/hatch.toml b/hatch.toml index aad1b874..52ba7b06 100644 --- a/hatch.toml +++ b/hatch.toml @@ -11,13 +11,9 @@ python = ["3.8", "3.9", "3.10", "3.11"] [envs.docs] python = "3.11" extras = ["docs"] -dependencies = ["scriv"] [envs.docs.scripts] build = "mkdocs build --clean --strict" serve = "mkdocs serve --dev-addr localhost:8000" init = "mkdocs {args}" -sync = ["entangled sync"] - -changelog-create = "scriv create {args}" -changelog-collect = "scriv collect {args}" \ No newline at end of file +sync = ["entangled sync"] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f6960ea9..7e7dd09d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -165,6 +165,3 @@ markers = [ "issue(id): reference to github issue", "skip_commit: allows to skip committing in the helpers", ] - -[tool.scriv] -format = "md" From e28bd9fb5a37df0be7261eba244b2aaa52f29517 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 16:59:04 +0200 Subject: [PATCH 061/162] dependency groups, uv and pytest-timeout --- .github/workflows/python-tests.yml | 6 ++- pyproject.toml | 17 ++++++--- testing/test_integration.py | 2 +- uv.lock | 59 +++++++++++++++++++++--------- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 393511fd..0cdfa54d 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -51,6 +51,8 @@ jobs: with: python-version: ${{ matrix.python_version }} architecture: x64 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v6 - name: Setup MSYS2 uses: msys2/setup-msys2@v2 if: matrix.python_version == 'msys2' @@ -81,13 +83,13 @@ jobs: echo "C:\Program Files (x86)\gnupg\bin" >> $env:GITHUB_PATH git config --system gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe" if: runner.os == 'Windows' - - run: pip install -U 'setuptools>=61' + - run: uv sync - uses: actions/download-artifact@v4 with: name: Packages path: dist - shell: bash - run: pip install "$(echo -n dist/*whl)[toml,test]" + run: uv pip install "$(echo -n dist/*whl)" - run: | $(hg debuginstall --template "{pythonexe}") -m pip install hg-git --user if: matrix.os == 'ubuntu-latest' diff --git a/pyproject.toml b/pyproject.toml index 7e7dd09d..80cb6443 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,13 @@ dependencies = [ 'typing-extensions; python_version < "3.10"', ] [project.optional-dependencies] +rich = [ + "rich", +] +toml = [ +] + +[dependency-groups] docs = [ #"entangled-cli~=2.0", "mkdocs", @@ -57,21 +64,17 @@ docs = [ "mkdocstrings[python]", "pygments", ] -rich = [ - "rich", -] test = [ "pip", "build", "pytest", + "pytest-timeout", # Timeout protection for CI/CD "rich", 'typing-extensions; python_version < "3.11"', "wheel", "griffe", "flake8", ] -toml = [ -] [project.urls] documentation = "https://setuptools-scm.readthedocs.io/" @@ -152,6 +155,7 @@ ignore = ["PP305", "GH103", "GH212", "MY100", "PC111", "PC160", "PC170", "PC180" [tool.pytest.ini_options] minversion = "8" testpaths = ["testing"] +timeout = 300 # 5 minutes timeout per test for CI protection filterwarnings = [ "error", "ignore:.*tool\\.setuptools_scm.*", @@ -165,3 +169,6 @@ markers = [ "issue(id): reference to github issue", "skip_commit: allows to skip committing in the helpers", ] + +[tool.uv] +default-groups = ["test", "docs"] diff --git a/testing/test_integration.py b/testing/test_integration.py index d688ec1d..f7654f4a 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -388,7 +388,7 @@ def test_distribution_provides_extras() -> None: dist = distribution("setuptools_scm") pe: list[str] = dist.metadata.get_all("Provides-Extra", []) - assert sorted(pe) == ["docs", "rich", "test", "toml"] + assert sorted(pe) == ["rich", "toml"] @pytest.mark.issue(760) diff --git a/uv.lock b/uv.lock index 81291d03..e28d70b0 100644 --- a/uv.lock +++ b/uv.lock @@ -1498,6 +1498,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1734,6 +1747,11 @@ dependencies = [ ] [package.optional-dependencies] +rich = [ + { name = "rich" }, +] + +[package.dev-dependencies] docs = [ { name = "mkdocs" }, { name = "mkdocs-entangled-plugin", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1745,9 +1763,6 @@ docs = [ { name = "mkdocstrings", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.9'" }, { name = "pygments" }, ] -rich = [ - { name = "rich" }, -] test = [ { name = "build" }, { name = "flake8", version = "5.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, @@ -1759,6 +1774,7 @@ test = [ { 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 = "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'" }, @@ -1767,27 +1783,34 @@ test = [ [package.metadata] requires-dist = [ - { name = "build", marker = "extra == 'test'" }, - { name = "flake8", marker = "extra == 'test'" }, - { name = "griffe", marker = "extra == 'test'" }, - { name = "mkdocs", marker = "extra == 'docs'" }, - { name = "mkdocs-entangled-plugin", marker = "extra == 'docs'" }, - { name = "mkdocs-include-markdown-plugin", marker = "extra == 'docs'" }, - { name = "mkdocs-material", marker = "extra == 'docs'" }, - { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'" }, { name = "packaging", specifier = ">=20" }, - { name = "pip", marker = "extra == 'test'" }, - { name = "pygments", marker = "extra == 'docs'" }, - { name = "pytest", marker = "extra == 'test'" }, { name = "rich", marker = "extra == 'rich'" }, - { name = "rich", marker = "extra == 'test'" }, { name = "setuptools" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11' and extra == 'test'" }, - { name = "wheel", marker = "extra == 'test'" }, ] -provides-extras = ["docs", "rich", "test", "toml"] +provides-extras = ["rich", "toml"] + +[package.metadata.requires-dev] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-entangled-plugin" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extras = ["python"] }, + { name = "pygments" }, +] +test = [ + { name = "build" }, + { name = "flake8" }, + { name = "griffe" }, + { name = "pip" }, + { name = "pytest" }, + { name = "pytest-timeout" }, + { name = "rich" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "wheel" }, +] [[package]] name = "six" From 5c4f6f0e0d6d037a2fa4cc73d097673ea26dabdb Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 17:04:23 +0200 Subject: [PATCH 062/162] fix readthedocs --- .readthedocs.yaml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b9ae71f4..5aa34e7a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,16 +1,15 @@ version: 2 -build: - os: ubuntu-22.04 - tools: - python: "3.11" mkdocs: configuration: mkdocs.yml -# Optionally declare the Python requirements required to build your docs -python: - install: - - method: pip - path: . - extra_requirements: - - docs \ No newline at end of file + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + jobs: + install: + - pip install -U pip # Official recommended way + - pip install . + - pip install --group docs From 21b47335716f54743b086677dc3f672b408dd7ef Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 17:06:00 +0200 Subject: [PATCH 063/162] sync all deps --- .github/workflows/python-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 0cdfa54d..f84eb69c 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -83,7 +83,7 @@ jobs: echo "C:\Program Files (x86)\gnupg\bin" >> $env:GITHUB_PATH git config --system gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe" if: runner.os == 'Windows' - - run: uv sync + - run: uv sync --group test --group docs --extra rich - uses: actions/download-artifact@v4 with: name: Packages From b138bbd2aae9a740e3dd9cbeb292a0cc40279b42 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 17:10:08 +0200 Subject: [PATCH 064/162] uv consistency --- .github/workflows/python-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index f84eb69c..faeeacdf 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -95,8 +95,8 @@ jobs: if: matrix.os == 'ubuntu-latest' # this hopefully helps with os caches, hg init sometimes gets 20s timeouts - run: hg version - - run: pytest - timeout-minutes: 15 + - run: uv run pytest + timeout-minutes: 25 dist_upload: From 0b65b8470728f026991473e02f0292ee7652a4c5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 21:11:46 +0200 Subject: [PATCH 065/162] test refinement: add wd_wrapper run timeout - default 10 seconds --- testing/wd_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py index e1aa6c4f..1f5efbe7 100644 --- a/testing/wd_wrapper.py +++ b/testing/wd_wrapper.py @@ -20,13 +20,13 @@ def __init__(self, cwd: Path) -> None: self.cwd = cwd self.__counter = itertools.count() - def __call__(self, cmd: list[str] | str, **kw: object) -> str: + def __call__(self, cmd: list[str] | str, *, timeout: int = 10, **kw: object) -> str: if kw: assert isinstance(cmd, str), "formatting the command requires text input" cmd = cmd.format(**kw) from setuptools_scm._run_cmd import run - return run(cmd, cwd=self.cwd).stdout + return run(cmd, cwd=self.cwd, timeout=timeout).stdout def write(self, name: str, content: str | bytes) -> Path: path = self.cwd / name From fc9f70887b0ad8be9e71b6c13a66a1906ee6ee0c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Jul 2025 22:59:21 +0200 Subject: [PATCH 066/162] sync hg repo setup --- testing/test_better_root_errors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/test_better_root_errors.py b/testing/test_better_root_errors.py index 16985ecc..31d7733d 100644 --- a/testing/test_better_root_errors.py +++ b/testing/test_better_root_errors.py @@ -31,9 +31,8 @@ def setup_hg_repo(wd: WorkDir) -> WorkDir: """Set up a mercurial repository for testing.""" try: wd("hg init") - wd("hg config --local ui.username 'test '") wd.add_command = "hg add ." - wd.commit_command = "hg commit -m test-{reason}" + wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' return wd except Exception: pytest.skip("hg not available") From 52ed4b972fd68ed2ecf59031d93bd5cf3eef3a49 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 1 Aug 2025 13:43:41 +0200 Subject: [PATCH 067/162] use the default logging lastResort instead of a own hackish copy closes #1139 --- CHANGELOG.md | 1 + src/setuptools_scm/_log.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7713d22..ea5b652f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - fix #324: document/recommend the v tag prefix - fix #501: add py.typed - fix #804: git - use fallback version instead of 0.0 when no version is found at all +- fix #1139: use logging.lastResort instead of a own replica to avoid polluting logging._handlerList ## v8.3.1 diff --git a/src/setuptools_scm/_log.py b/src/setuptools_scm/_log.py index 7de41231..ea17f375 100644 --- a/src/setuptools_scm/_log.py +++ b/src/setuptools_scm/_log.py @@ -39,9 +39,9 @@ def make_default_handler() -> logging.Handler: return RichHandler(console=console) except ImportError: - handler = AlwaysStdErrHandler() - handler.setFormatter(logging.Formatter("%(levelname)s %(name)s %(message)s")) - return handler + last_resort = logging.lastResort + assert last_resort is not None + return last_resort _default_handler = make_default_handler() From 53503786bb8bd1e101189363983699d196a53b03 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 1 Aug 2025 14:09:51 +0200 Subject: [PATCH 068/162] dont infer version in cli if --no-version is given closes #873 --- CHANGELOG.md | 1 + src/setuptools_scm/_cli.py | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5b652f..ca407920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - fix #501: add py.typed - fix #804: git - use fallback version instead of 0.0 when no version is found at all - fix #1139: use logging.lastResort instead of a own replica to avoid polluting logging._handlerList +- fix #873: don't infer version in cli if --no-version is given ## v8.3.1 diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py index b54903a4..3f33f83d 100644 --- a/src/setuptools_scm/_cli.py +++ b/src/setuptools_scm/_cli.py @@ -33,10 +33,13 @@ def main(args: list[str] | None = None) -> int: file=sys.stderr, ) config = Configuration(root=inferred_root) - - version = _get_version( - config, force_write_version_files=opts.force_write_version_files - ) + version: str | None + if opts.no_version: + version = "0.0.0+no-version-was-requested.fake-version" + else: + version = _get_version( + config, force_write_version_files=opts.force_write_version_files + ) if version is None: raise SystemExit("ERROR: no version found for", opts) if opts.strip_dev: From 89598011e8b625ea9b3605837609aa1bc9de4f48 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 1 Aug 2025 14:55:34 +0200 Subject: [PATCH 069/162] accept tags from the ui release action for dist uploads closes #535 --- .github/workflows/python-tests.yml | 4 +++- CHANGELOG.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index faeeacdf..5aad6dda 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -7,6 +7,8 @@ on: - "*" tags: - "v*" + release: + types: [published] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} @@ -101,7 +103,7 @@ jobs: dist_upload: runs-on: ubuntu-latest - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + if: (github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')) || (github.event_name == 'release' && github.event.action == 'published') permissions: id-token: write needs: [test] diff --git a/CHANGELOG.md b/CHANGELOG.md index ca407920..98dceeff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - fix #804: git - use fallback version instead of 0.0 when no version is found at all - fix #1139: use logging.lastResort instead of a own replica to avoid polluting logging._handlerList - fix #873: don't infer version in cli if --no-version is given +- fix #535: accept tags from a release action in the gh ui ## v8.3.1 From 2a25bd6050822a8f942f5c129466f46325d85c42 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 1 Aug 2025 15:17:27 +0200 Subject: [PATCH 070/162] expand documentation on git archival/gitattributes and add cli tools for creating them closes #987 --- CHANGELOG.md | 2 + docs/usage.md | 116 +++++++++++++++++++++++++-- src/setuptools_scm/_cli.py | 102 +++++++++++++++++++++++ testing/test_cli.py | 160 +++++++++++++++++++++++++++++++++++++ 4 files changed, 374 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca407920..52c86e28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - 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 - add `scm` parameter support to `get_version()` function for nested SCM configuration +- fix #987: expand documentation on git archival files and add cli tools for good defaults + ### Changed - add `pip` to test optional dependencies for improved uv venv compatibility diff --git a/docs/usage.md b/docs/usage.md index d48a4252..efa922cb 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -376,15 +376,41 @@ accordingly. ### Git archives -Git archives are supported, but a few changes to your repository are required. +Git archives are supported, but require specific setup and understanding of how they work with package building. -Ensure the content of the following files: +#### Overview +When you create a `.git_archival.txt` file in your repository, it enables setuptools-scm to extract version information from git archives (e.g., GitHub's source downloads). However, this file contains template placeholders that must be expanded by `git archive` - they won't work when building directly from your working directory. + +#### Setting up git archival support + +You can generate a `.git_archival.txt` file using the setuptools-scm CLI: + +```commandline +# Generate a stable archival file (recommended for releases) +$ python -m setuptools_scm create-archival-file --stable + +# Generate a full archival file with all metadata (use with caution) +$ python -m setuptools_scm create-archival-file --full +``` + +Alternatively, you can create the file manually: + +**Stable version (recommended):** ```{ .text file=".git_archival.txt"} +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +``` +**Full version (with branch information - can cause instability):** +```{ .text file=".git_archival.txt"} +# WARNING: Including ref-names can make archive checksums unstable +# after commits are added post-release. Use only if describe-name is insufficient. node: $Format:%H$ node-date: $Format:%cI$ describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ ``` Feel free to alter the `match` field in `describe-name` to match your project's @@ -402,14 +428,92 @@ tagging style. .git_archival.txt export-subst ``` -Finally, don't forget to commit the two files: +Finally, commit both files: ```commandline -$ git add .git_archival.txt .gitattributes && git commit -m "add export config" +$ git add .git_archival.txt .gitattributes && git commit -m "add git archive support" +``` + +#### Understanding the warnings + +If you see warnings like these when building your package: + ``` +UserWarning: git archive did not support describe output +UserWarning: unprocessed git archival found (no export subst applied) +``` + +This typically happens when: + +1. **Building from working directory**: You're running `python -m build` directly in your repository +2. **Sdist extraction**: A build tool extracts your sdist to build wheels, but the extracted directory isn't a git repository + +#### Recommended build workflows + +**For development builds:** +Exclude `.git_archival.txt` from your package to avoid warnings: + +```{ .text file="MANIFEST.in"} +# Exclude archival file from development builds +exclude .git_archival.txt +``` + +**For release builds from archives:** +Build from an actual git archive to ensure proper template expansion: + +```commandline +# Create archive from a specific tag/commit +$ git archive --output=../source_archive.tar v1.2.3 +$ cd .. +$ tar -xf source_archive.tar +$ cd extracted_directory/ +$ python -m build . +``` + +**For automated releases:** +Many CI systems and package repositories (like GitHub Actions) automatically handle this correctly when building from git archives. + +#### Integration with package managers + +**MANIFEST.in exclusions:** +```{ .text file="MANIFEST.in"} +# Exclude development files from packages +exclude .git_archival.txt +exclude .gitattributes +``` + + +```{ .text file=".gitattributes"} +# Archive configuration +.git_archival.txt export-subst +.gitignore export-ignore +``` + +#### Troubleshooting + +**Problem: "unprocessed git archival found" warnings** +- ✅ **Solution**: Add `exclude .git_archival.txt` to `MANIFEST.in` for development builds +- ✅ **Alternative**: Build from actual git archives for releases + +**Problem: "git archive did not support describe output" warnings** +- ℹ️ **Information**: This is expected when `.git_archival.txt` contains unexpanded templates +- ✅ **Solution**: Same as above - exclude file or build from git archives + +**Problem: Version detection fails in git archives** +- ✅ **Check**: Is `.gitattributes` configured with `export-subst`? +- ✅ **Check**: Are you building from a properly created git archive? +- ✅ **Check**: Does your git hosting provider support archive template expansion? + +!!! warning "Branch Names and Archive Stability" + + Including `ref-names: $Format:%D$` in your `.git_archival.txt` can make archive checksums change when new commits are added to branches referenced in the archive. This primarily affects GitHub's automatic source archives. Use the stable format (without `ref-names`) unless you specifically need branch information and understand the stability implications. +!!! note "Version Files" -Note that if you are creating a `_version.py` file, note that it should not -be kept in version control. It's strongly recommended to be put into gitignore. + If you are creating a `_version.py` file, it should not be kept in version control. Add it to `.gitignore`: + ``` + # Generated version file + src/mypackage/_version.py + ``` [git-archive-issue]: https://github.com/pypa/setuptools-scm/issues/806 diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py index 3f33f83d..1f104f46 100644 --- a/src/setuptools_scm/_cli.py +++ b/src/setuptools_scm/_cli.py @@ -5,6 +5,7 @@ import os import sys +from pathlib import Path from typing import Any from setuptools_scm import Configuration @@ -106,6 +107,28 @@ def _get_cli_opts(args: list[str] | None) -> argparse.Namespace: # We avoid `metavar` to prevent printing repetitive information desc = "List information about the package, e.g. included files" sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc) + + # Add create-archival-file subcommand + archival_desc = "Create .git_archival.txt file for git archive support" + archival_parser = sub.add_parser( + "create-archival-file", + help=archival_desc[0].lower() + archival_desc[1:], + description=archival_desc, + ) + archival_group = archival_parser.add_mutually_exclusive_group(required=True) + archival_group.add_argument( + "--stable", + action="store_true", + help="create stable archival file (recommended, no branch names)", + ) + archival_group.add_argument( + "--full", + action="store_true", + help="create full archival file with branch information (can cause instability)", + ) + archival_parser.add_argument( + "--force", action="store_true", help="overwrite existing .git_archival.txt file" + ) return parser.parse_args(args) @@ -116,6 +139,9 @@ def command(opts: argparse.Namespace, version: str, config: Configuration) -> in if opts.command == "ls": opts.query = ["files"] + if opts.command == "create-archival-file": + return _create_archival_file(opts, config) + if opts.query == []: opts.no_version = True sys.stderr.write("Available queries:\n\n") @@ -187,3 +213,79 @@ def _find_pyproject(parent: str) -> str: return os.path.abspath( "pyproject.toml" ) # use default name to trigger the default errors + + +def _create_archival_file(opts: argparse.Namespace, config: Configuration) -> int: + """Create .git_archival.txt file with appropriate content.""" + archival_path = Path(config.root, ".git_archival.txt") + + # Check if file exists and force flag + if archival_path.exists() and not opts.force: + print( + f"Error: {archival_path} already exists. Use --force to overwrite.", + file=sys.stderr, + ) + return 1 + + if opts.stable: + content = _get_stable_archival_content() + print("Creating stable .git_archival.txt (recommended for releases)") + elif opts.full: + content = _get_full_archival_content() + print("Creating full .git_archival.txt with branch information") + print("WARNING: This can cause archive checksums to be unstable!") + + try: + archival_path.write_text(content, encoding="utf-8") + print(f"Created: {archival_path}") + + gitattributes_path = Path(config.root, ".gitattributes") + needs_gitattributes = True + + if gitattributes_path.exists(): + # TODO: more nuanced check later + gitattributes_content = gitattributes_path.read_text("utf-8") + if ( + ".git_archival.txt" in gitattributes_content + and "export-subst" in gitattributes_content + ): + needs_gitattributes = False + + if needs_gitattributes: + print("\nNext steps:") + print("1. Add this line to .gitattributes:") + print(" .git_archival.txt export-subst") + print("2. Commit both files:") + print(" git add .git_archival.txt .gitattributes") + print(" git commit -m 'add git archive support'") + else: + print("\nNext step:") + print("Commit the archival file:") + print(" git add .git_archival.txt") + print(" git commit -m 'update git archival file'") + + return 0 + except OSError as e: + print(f"Error: Could not create {archival_path}: {e}", file=sys.stderr) + return 1 + + +def _get_stable_archival_content() -> str: + """Generate stable archival file content (no branch names).""" + return """\ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +""" + + +def _get_full_archival_content() -> str: + """Generate full archival file content with branch information.""" + return """\ +# WARNING: Including ref-names can make archive checksums unstable +# after commits are added post-release. Use only if describe-name is insufficient. +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ +""" diff --git a/testing/test_cli.py b/testing/test_cli.py index 050fe031..480793c5 100644 --- a/testing/test_cli.py +++ b/testing/test_cli.py @@ -80,3 +80,163 @@ def test_cli_force_version_files( assert version_file.exists() assert output[:5] in version_file.read_text("utf-8") + + +def test_cli_create_archival_file_stable( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test creating stable .git_archival.txt file.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + archival_file = wd.cwd / ".git_archival.txt" + assert not archival_file.exists() + + # Test successful creation + result = main(["create-archival-file", "--stable"]) + assert result == 0 + assert archival_file.exists() + + content = archival_file.read_text("utf-8") + expected_lines = [ + "node: $Format:%H$", + "node-date: $Format:%cI$", + "describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$", + ] + for line in expected_lines: + assert line in content + + # Stable version should not contain ref-names + assert "ref-names" not in content + + +def test_cli_create_archival_file_full( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test creating full .git_archival.txt file with branch information.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + archival_file = wd.cwd / ".git_archival.txt" + assert not archival_file.exists() + + # Test successful creation + result = main(["create-archival-file", "--full"]) + assert result == 0 + assert archival_file.exists() + + content = archival_file.read_text("utf-8") + expected_lines = [ + "node: $Format:%H$", + "node-date: $Format:%cI$", + "describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$", + "ref-names: $Format:%D$", + ] + for line in expected_lines: + assert line in content + + # Full version should contain warning comment + assert "WARNING" in content + assert "unstable" in content + + +def test_cli_create_archival_file_exists_no_force( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that existing .git_archival.txt file prevents creation without --force.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + archival_file = wd.cwd / ".git_archival.txt" + archival_file.write_text("existing content", encoding="utf-8") + + # Should fail without --force + result = main(["create-archival-file", "--stable"]) + assert result == 1 + + # Content should be unchanged + assert archival_file.read_text("utf-8") == "existing content" + + +def test_cli_create_archival_file_exists_with_force( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that --force overwrites existing .git_archival.txt file.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + archival_file = wd.cwd / ".git_archival.txt" + archival_file.write_text("existing content", encoding="utf-8") + + # Should succeed with --force + result = main(["create-archival-file", "--stable", "--force"]) + assert result == 0 + + # Content should be updated + content = archival_file.read_text("utf-8") + assert "existing content" not in content + assert "node: $Format:%H$" in content + + +def test_cli_create_archival_file_requires_stable_or_full( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that create-archival-file requires either --stable or --full.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + # Should fail without --stable or --full + with pytest.raises(SystemExit): + main(["create-archival-file"]) + + +def test_cli_create_archival_file_mutually_exclusive( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that --stable and --full are mutually exclusive.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + # Should fail with both --stable and --full + with pytest.raises(SystemExit): + main(["create-archival-file", "--stable", "--full"]) + + +def test_cli_create_archival_file_existing_gitattributes( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test behavior when .gitattributes already has export-subst configuration.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + # Create .gitattributes with export-subst configuration + gitattributes_file = wd.cwd / ".gitattributes" + gitattributes_file.write_text(".git_archival.txt export-subst\n", encoding="utf-8") + + result = main(["create-archival-file", "--stable"]) + assert result == 0 + + archival_file = wd.cwd / ".git_archival.txt" + assert archival_file.exists() + + +def test_cli_create_archival_file_no_gitattributes( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test behavior when .gitattributes doesn't exist or lacks export-subst.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + result = main(["create-archival-file", "--stable"]) + assert result == 0 + + archival_file = wd.cwd / ".git_archival.txt" + assert archival_file.exists() From 33c99f6af97e4053c5f56b44105353b2dc15400c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 1 Aug 2025 15:50:24 +0200 Subject: [PATCH 071/162] document recommend ci additions for pypi/test-pypi uploads closes #311 --- CHANGELOG.md | 2 + docs/integrations.md | 295 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 296 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca2177d9..26e25b2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - fix #1059: add `SETUPTOOLS_SCM_PRETEND_METADATA` environment variable to override individual ScmVersion fields - add `scm` parameter support to `get_version()` function for nested SCM configuration - fix #987: expand documentation on git archival files and add cli tools for good defaults +- fix #311: document github/gitlab ci pipelines that enable auto-upload to test-pypi/pypi + ### Changed diff --git a/docs/integrations.md b/docs/integrations.md index ba097fe6..5c6fa8d3 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -47,4 +47,297 @@ build: - export SETUPTOOLS_SCM_OVERRIDES_FOR_${READTHEDOCS_PROJECT//-/_}='{scm.git.pre_parse="fail_on_shallow"}' ``` -This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow. \ No newline at end of file +This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow. + +## CI/CD and Package Publishing + +### Publishing to PyPI from CI/CD + +When publishing packages to PyPI or test-PyPI from CI/CD pipelines, you often need to remove local version components that are not allowed on public package indexes according to [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers). + +setuptools-scm provides the `no-local-version` local scheme and environment variable overrides to handle this scenario cleanly. + +#### The Problem + +By default, setuptools-scm generates version numbers like: +- `1.2.3.dev4+g1a2b3c4d5` (development version with git hash) +- `1.2.3+dirty` (dirty working directory) + +These local version components (`+g1a2b3c4d5`, `+dirty`) prevent uploading to PyPI. + +#### The Solution + +Use the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `local_scheme` to `no-local-version` when building for upload to PyPI. + +### GitHub Actions Example + +Here's a complete GitHub Actions workflow that: +- Runs tests on all branches +- Uploads development versions to test-PyPI from feature branches +- Uploads development versions to PyPI from the main branch (with no-local-version) +- Uploads tagged releases to PyPI (using exact tag versions) + +```yaml title=".github/workflows/ci.yml" +name: CI/CD + +on: + push: + branches: ["main", "develop"] + pull_request: + branches: ["main", "develop"] + release: + types: [published] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + with: + # Fetch full history for setuptools-scm + fetch-depth: 0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build pytest + pip install -e . + + - name: Run tests + run: pytest + + publish-test-pypi: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref != 'refs/heads/main' + env: + # Replace MYPACKAGE with your actual package name (normalized) + # For package "my-awesome.package", use "MY_AWESOME_PACKAGE" + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Upload to test-PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + + publish-pypi: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + # Replace MYPACKAGE with your actual package name (normalized) + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + + publish-release: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'release' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} +``` + +### GitLab CI Example + +Here's an equivalent GitLab CI configuration: + +```yaml title=".gitlab-ci.yml" +stages: + - test + - publish + +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + +cache: + paths: + - .cache/pip/ + +before_script: + - python -m pip install --upgrade pip + +test: + stage: test + image: python:3.11 + script: + - pip install build pytest + - pip install -e . + - pytest + parallel: + matrix: + - PYTHON_VERSION: ["3.8", "3.9", "3.10", "3.11", "3.12"] + image: python:${PYTHON_VERSION} + +publish-test-pypi: + stage: publish + image: python:3.11 + variables: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: $TEST_PYPI_API_TOKEN + # Replace MYPACKAGE with your actual package name (normalized) + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}' + script: + - pip install build twine + - python -m build + - twine upload --repository testpypi dist/* + rules: + - if: $CI_COMMIT_BRANCH != "main" && $CI_PIPELINE_SOURCE == "push" + +publish-pypi: + stage: publish + image: python:3.11 + variables: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: $PYPI_API_TOKEN + # Replace MYPACKAGE with your actual package name (normalized) + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}' + script: + - pip install build twine + - python -m build + - twine upload dist/* + rules: + - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push" + +publish-release: + stage: publish + image: python:3.11 + variables: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: $PYPI_API_TOKEN + script: + - pip install build twine + - python -m build + - twine upload dist/* + rules: + - if: $CI_COMMIT_TAG +``` + +### Configuration Details + +#### Environment Variable Format + +The environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` must be set where: + +1. **`${NORMALIZED_DIST_NAME}`** is your package name normalized according to PEP 503: + - Convert to uppercase + - Replace hyphens and dots with underscores + - Examples: `my-package` → `MY_PACKAGE`, `my.package` → `MY_PACKAGE` + +2. **Value** must be a valid TOML inline table format: + ```bash + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE='{"local_scheme": "no-local-version"}' + ``` + +#### Alternative: pyproject.toml Configuration + +Instead of environment variables, you can configure this in your `pyproject.toml`: + +```toml title="pyproject.toml" +[tool.setuptools_scm] +# Use no-local-version by default for CI builds +local_scheme = "no-local-version" +``` + +However, the environment variable approach is preferred for CI/CD as it allows different schemes for local development vs. CI builds. + +#### Version Examples + +**Development versions from main branch** (with `local_scheme = "no-local-version"`): +- Development commit: `1.2.3.dev4+g1a2b3c4d5` → `1.2.3.dev4` ✅ (uploadable to PyPI) +- Dirty working directory: `1.2.3+dirty` → `1.2.3` ✅ (uploadable to PyPI) + +**Tagged releases** (without overrides, using default local scheme): +- Tagged commit: `1.2.3` → `1.2.3` ✅ (uploadable to PyPI) +- Tagged release on dirty workdir: `1.2.3+dirty` → `1.2.3+dirty` ❌ (should not happen in CI) + +### Security Notes + +- Store PyPI API tokens as repository secrets +- Use separate tokens for test-PyPI and production PyPI +- Consider using [Trusted Publishers](https://docs.pypi.org/trusted-publishers/) for enhanced security + +### Troubleshooting + +**Package name normalization**: If your override isn't working, verify the package name normalization: + +```python +import re +dist_name = "my-awesome.package" +normalized = re.sub(r"[-_.]+", "-", dist_name) +env_var_name = normalized.replace("-", "_").upper() +print(f"SETUPTOOLS_SCM_OVERRIDES_FOR_{env_var_name}") +# Output: SETUPTOOLS_SCM_OVERRIDES_FOR_MY_AWESOME_PACKAGE +``` + +**Fetch depth**: Always use `fetch-depth: 0` in GitHub Actions to ensure setuptools-scm has access to the full git history for proper version calculation. \ No newline at end of file From 538251c9a12d70a9ade1cbdf1395ef0c23f33682 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 1 Aug 2025 16:02:10 +0200 Subject: [PATCH 072/162] add test-pypi upload for main --- .github/workflows/python-tests.yml | 4 ++++ _own_version_helper.py | 11 ++++++++++- docs/integrations.md | 4 +++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 5aad6dda..77dd1129 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -22,6 +22,9 @@ jobs: package: name: Build & inspect our package. runs-on: ubuntu-latest + env: + # Use no-local-version for package builds to ensure clean versions for PyPI uploads + SETUPTOOLS_SCM_NO_LOCAL: "1" steps: - uses: actions/checkout@v4 @@ -117,6 +120,7 @@ jobs: test-pypi-upload: runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: [test] permissions: id-token: write diff --git a/_own_version_helper.py b/_own_version_helper.py index d0d7433c..12ffeb07 100644 --- a/_own_version_helper.py +++ b/_own_version_helper.py @@ -9,6 +9,7 @@ from __future__ import annotations import logging +import os from typing import Callable @@ -22,6 +23,7 @@ from setuptools_scm.fallbacks import parse_pkginfo from setuptools_scm.version import ScmVersion from setuptools_scm.version import get_local_node_and_date +from setuptools_scm.version import get_no_local_node from setuptools_scm.version import guess_next_dev_version log = logging.getLogger("setuptools_scm") @@ -48,11 +50,18 @@ def parse(root: str, config: Configuration) -> ScmVersion | None: def scm_version() -> str: + # Use no-local-version if SETUPTOOLS_SCM_NO_LOCAL is set (for CI uploads) + local_scheme = ( + get_no_local_node + if os.environ.get("SETUPTOOLS_SCM_NO_LOCAL") + else get_local_node_and_date + ) + return get_version( relative_to=__file__, parse=parse, version_scheme=guess_next_dev_version, - local_scheme=get_local_node_and_date, + local_scheme=local_scheme, ) diff --git a/docs/integrations.md b/docs/integrations.md index 5c6fa8d3..e94a6ea0 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -299,7 +299,9 @@ The environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE='{"local_scheme": "no-local-version"}' ``` -#### Alternative: pyproject.toml Configuration +#### Alternative Approaches + +**Option 1: pyproject.toml Configuration** Instead of environment variables, you can configure this in your `pyproject.toml`: From dc4b02027a8e167af0e1baa99075d89e1f8e4e99 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 1 Aug 2025 16:15:39 +0200 Subject: [PATCH 073/162] chore: allow unraisable exceptions again, seems we no longer trigger the error --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 80cb6443..a8345310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,7 +164,7 @@ filterwarnings = [ log_level = "debug" log_cli_level = "info" # disable unraisable until investigated -addopts = ["-ra", "--strict-config", "--strict-markers", "-p", "no:unraisableexception"] +addopts = ["-ra", "--strict-config", "--strict-markers"] markers = [ "issue(id): reference to github issue", "skip_commit: allows to skip committing in the helpers", From 6fb72b71aec476594e7fe57477b3ac0fc0c304d9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 2 Aug 2025 09:26:30 +0200 Subject: [PATCH 074/162] Enable PYTHONTRACEMALLOC on Windows CI and add CI/CD docs - Add PYTHONTRACEMALLOC=1 on Windows runners to debug gc errors with popen objects in test_case_cwd_evil[git] --- .github/workflows/python-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 77dd1129..89815baa 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -44,6 +44,9 @@ jobs: include: - os: windows-latest python_version: 'msys2' + env: + # Enable tracemalloc to debug gc errors with popen objects (especially on Windows) + PYTHONTRACEMALLOC: "1" name: ${{ matrix.os }} - Python ${{ matrix.python_version }} steps: From 2aee615db063fb43e548ec61ed96e21376932cd0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 2 Aug 2025 09:57:03 +0200 Subject: [PATCH 075/162] Fix popen cleanup in git file finder to prevent gc errors - Add proper proc.wait() call in finally block to ensure process is reaped - Add timeout and fallback to proc.kill() to prevent hanging - This should resolve gc errors with popen objects in test_case_cwd_evil[git] on Windows The previous code only called proc.wait() in the exception handler, leaving processes unreleased in the normal flow, which could cause gc warnings about unclosed subprocess popen objects. --- src/setuptools_scm/_file_finders/git.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/setuptools_scm/_file_finders/git.py b/src/setuptools_scm/_file_finders/git.py index 0eb23ced..fe6dfb05 100644 --- a/src/setuptools_scm/_file_finders/git.py +++ b/src/setuptools_scm/_file_finders/git.py @@ -84,8 +84,16 @@ def _git_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: # ensure we avoid resource warnings by cleaning up the process proc.stdout.close() proc.terminate() + # Wait for process to actually terminate and be reaped + try: + proc.wait(timeout=5) # Add timeout to avoid hanging + except subprocess.TimeoutExpired: + log.warning("git archive process did not terminate gracefully, killing") + proc.kill() + proc.wait() except Exception: - if proc.wait() != 0: + # proc.wait() already called in finally block, check if it failed + if proc.returncode != 0: log.error("listing git files failed - pretending there aren't any") return set(), set() From 74c6b19fbe04455b4046f093bf7cfb0d471f7cac Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 2 Aug 2025 11:27:02 +0200 Subject: [PATCH 076/162] mention what namespaces are for release-branch-semver --- CHANGELOG.md | 1 + docs/extending.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e25b2b..dd895f45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - fix #1139: use logging.lastResort instead of a own replica to avoid polluting logging._handlerList - fix #873: don't infer version in cli if --no-version is given - fix #535: accept tags from a release action in the gh ui +- fix #1073: explain namespaces for release-branch-semver ## v8.3.1 diff --git a/docs/extending.md b/docs/extending.md index 0436b138..c4cc2e03 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -120,8 +120,12 @@ representing the version. non-release branch, increments the minor segment and sets the micro segment to zero, then appends `.devN` + Namespaces are unix pathname separated parts of a branch/tag name. + **Examples:** - Tag `1.0.0` on release branch `release-1.0` → version `1.0.1.devN` + + - Tag `1.0.0` on release branch `release/v1.0` → version `1.0.1.devN` - Tag `1.0.0` on development branch → version `1.1.0.devN` ### `setuptools_scm.local_scheme` From 63be5d47cb43b1a02bbfcd0bc8b4e05d145c0e65 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 2 Aug 2025 12:58:04 +0200 Subject: [PATCH 077/162] Add failing test for issue #1022: version_keyword should override infer_version This test demonstrates the bug where version_keyword with specific configuration (like calver-by-date) doesn't properly override the version already set by infer_version. The test intentionally fails to show the current behavior vs the desired behavior where version_keyword should always win when it provides additional configuration. Test uses SETUPTOOLS_SCM_PRETEND_METADATA to ensure deterministic calver dates. --- testing/test_integration.py | 117 ++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/testing/test_integration.py b/testing/test_integration.py index f7654f4a..c4bfcb74 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -8,11 +8,16 @@ import textwrap from pathlib import Path +from typing import TYPE_CHECKING +from typing import Any import pytest import setuptools_scm._integration.setuptools +if TYPE_CHECKING: + import 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 @@ -674,3 +679,115 @@ def test_improved_error_message_mentions_both_config_options( assert "tool.setuptools_scm" in error_msg assert "build-system" in error_msg assert "requires" in error_msg + + +# Helper functions for testing integration point ordering +def integration_infer_version(dist: setuptools.Distribution) -> str: + """Helper to call infer_version and return the result.""" + from setuptools_scm._integration.setuptools import infer_version + + infer_version(dist) + return "infer_version" + + +def integration_version_keyword_default(dist: setuptools.Distribution) -> str: + """Helper to call version_keyword with default config and return the result.""" + from setuptools_scm._integration.setuptools import version_keyword + + version_keyword(dist, "use_scm_version", True) + return "version_keyword_default" + + +def integration_version_keyword_calver(dist: setuptools.Distribution) -> str: + """Helper to call version_keyword with calver-by-date scheme and return the result.""" + from setuptools_scm._integration.setuptools import version_keyword + + version_keyword(dist, "use_scm_version", {"version_scheme": "calver-by-date"}) + return "version_keyword_calver" + + +# 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 + (integration_infer_version, integration_version_keyword_default, "1.0.1.dev1"), + ( + integration_infer_version, + integration_version_keyword_calver, + "9.2.13.0.dev1", + ), # calver should win but doesn't + (integration_version_keyword_default, integration_infer_version, "1.0.1.dev1"), + (integration_version_keyword_calver, integration_infer_version, "9.2.13.0.dev1"), +] + + +@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/1022") +@pytest.mark.filterwarnings("ignore:version of .* already set:UserWarning") +@pytest.mark.filterwarnings( + "ignore:.* does not correspond to a valid versioning date.*:UserWarning" +) +@pytest.mark.parametrize( + ("first_integration", "second_integration", "expected_final_version"), + TEST_CASES, +) +def test_integration_function_call_order( + wd: WorkDir, + monkeypatch: pytest.MonkeyPatch, + first_integration: Any, + second_integration: Any, + expected_final_version: str, +) -> None: + """Test that integration functions can be called in any order. + + version_keyword should always win when it specifies configuration, but currently doesn't. + Some tests will fail, showing the bug. + """ + # 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}") + + # Set up a git repository with a tag and known commit hash + wd.commit_testfile("test") + wd("git tag 1.0.0") + wd.commit_testfile("test2") # Add another commit to get distance + monkeypatch.chdir(wd.cwd) + + # Generate unique distribution name based on the test combination + first_name = first_integration.__name__.replace("integration_", "") + second_name = second_integration.__name__.replace("integration_", "") + dist_name = f"test-pkg-{first_name}-then-{second_name}" + + # Create a pyproject.toml file + pyproject_content = f""" +[build-system] +requires = ["setuptools", "setuptools_scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "{dist_name}" +dynamic = ["version"] + +[tool.setuptools_scm] +local_scheme = "no-local-version" +""" + wd.write("pyproject.toml", pyproject_content) + + import setuptools + + # Create distribution and clear any auto-set version + dist = setuptools.Distribution({"name": dist_name}) + dist.metadata.version = None + + # Call both integration functions in order + first_integration(dist) + second_integration(dist) + + # Get the final version directly from the distribution + final_version = dist.metadata.version + + # Assert the final version matches expectation + # Some tests will fail here, demonstrating the bug where version_keyword doesn't override + assert final_version == expected_final_version, ( + f"Expected version '{expected_final_version}' but got '{final_version}'" + ) From 47ec4abae449e2b9a2c770d59210c6a7661d8455 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 2 Aug 2025 13:08:39 +0200 Subject: [PATCH 078/162] Fix issue #1022: version_keyword should override infer_version when config differs When infer_version sets a version first, version_keyword should be able to override it if it brings additional configuration (like a different version_scheme). This commit implements a marker system: - infer_version sets _setuptools_scm_version_set_by_infer=True when it sets a version - version_keyword checks this marker and: - If no overrides: uses infer_version result (no warning) - If overrides present: clears version and recalculates with new config - If version set by other means: warns as before This allows proper integration behavior where version_keyword can override infer_version when the user provides specific configuration like calver schemes. --- CHANGELOG.md | 1 + src/setuptools_scm/_integration/setuptools.py | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd895f45..9d6d4ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - add `scm` parameter support to `get_version()` function for nested SCM configuration - fix #987: expand documentation on git archival files and add cli tools for good defaults - fix #311: document github/gitlab ci pipelines that enable auto-upload to test-pypi/pypi +- fix #1022: allow `version_keyword` to override `infer_version` when configuration differs ### Changed diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 7a9a577a..242f4152 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -108,8 +108,21 @@ def version_keyword( _log_hookstart("version_keyword", dist) if dist.metadata.version is not None: - warnings.warn(f"version of {dist_name} already set") - return + # Check if version was set by infer_version + was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) + + if was_set_by_infer: + # Version was set by infer_version, check if we have overrides + if not overrides: + # No overrides, just use the infer_version result + return + # We have overrides, clear the marker and proceed to override the version + dist._setuptools_scm_version_set_by_infer = False # type: ignore[attr-defined] + dist.metadata.version = None + else: + # Version was set by something else, warn and return + warnings.warn(f"version of {dist_name} already set") + return if dist_name is None: dist_name = read_dist_name_from_setup_cfg() @@ -141,3 +154,5 @@ def infer_version(dist: setuptools.Distribution) -> None: log.info(e, exc_info=True) else: _assign_version(dist, config) + # Mark that this version was set by infer_version + dist._setuptools_scm_version_set_by_infer = True # type: ignore[attr-defined] From 82587cc172e3f6dfcadd844a3eef57a3b0960a99 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 2 Aug 2025 21:41:39 +0200 Subject: [PATCH 079/162] first working iteration on build tag passover from scm tags to version strings addresses #1019 needs more iteration --- src/setuptools_scm/version.py | 119 +++++++++++++++++++++++++++-- testing/test_functions.py | 102 +++++++++++++++++++++++++ testing/test_integration.py | 136 ++++++++++++++++++++++++++++++++++ 3 files changed, 351 insertions(+), 6 deletions(-) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 69b184eb..6b18a092 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -111,11 +111,31 @@ def tag_to_version( version_str = tag_dict["version"] log.debug("version pre parse %s", version_str) - if suffix := tag_dict.get("suffix", ""): - warnings.warn(f"tag {tag!r} will be stripped of its suffix {suffix!r}") + # Try to create version from base version first + try: + version: _VersionT = config.version_cls(version_str) + log.debug("version=%r", version) + except Exception: + warnings.warn( + f"tag {tag!r} will be stripped of its suffix {tag_dict.get('suffix', '')!r}" + ) + # Fall back to trying without any suffix + version = config.version_cls(version_str) + log.debug("version=%r", version) + return version - version: _VersionT = config.version_cls(version_str) - log.debug("version=%r", version) + # If base version is valid, check if we can preserve the suffix + if suffix := tag_dict.get("suffix", ""): + log.debug("tag %r includes local build data %r, preserving it", tag, suffix) + # Try creating version with suffix - if it fails, we'll use the base version + try: + version_with_suffix = config.version_cls(version_str + suffix) + log.debug("version with suffix=%r", version_with_suffix) + return version_with_suffix + except Exception: + warnings.warn(f"tag {tag!r} will be stripped of its suffix {suffix!r}") + # Return the base version without suffix + return version return version @@ -455,6 +475,60 @@ def postrelease_version(version: ScmVersion) -> str: return version.format_with("{tag}.post{distance}") +def _combine_version_with_local_parts( + main_version: str, *local_parts: str | None +) -> str: + """ + Combine a main version with multiple local parts into a valid PEP 440 version string. + Handles deduplication of local parts to avoid adding the same local data twice. + + Args: + main_version: The main version string (e.g., "1.2.0", "1.2.dev3") + *local_parts: Variable number of local version parts, can be None or empty + + Returns: + A valid PEP 440 version string + + Examples: + _combine_version_with_local_parts("1.2.0", "build.123", "d20090213") -> "1.2.0+build.123.d20090213" + _combine_version_with_local_parts("1.2.0", "build.123", None) -> "1.2.0+build.123" + _combine_version_with_local_parts("1.2.0+build.123", "d20090213") -> "1.2.0+build.123.d20090213" + _combine_version_with_local_parts("1.2.0+build.123", "build.123") -> "1.2.0+build.123" # no duplication + _combine_version_with_local_parts("1.2.0", None, None) -> "1.2.0" + """ + # Split main version into base and existing local parts + if "+" in main_version: + main_part, existing_local = main_version.split("+", 1) + all_local_parts = existing_local.split(".") + else: + main_part = main_version + all_local_parts = [] + + # Process each new local part + for part in local_parts: + if not part or not part.strip(): + continue + + # Strip any leading + and split into segments + clean_part = part.strip("+") + if not clean_part: + continue + + # Split multi-part local identifiers (e.g., "build.123" -> ["build", "123"]) + part_segments = clean_part.split(".") + + # Add each segment if not already present + for segment in part_segments: + if segment and segment not in all_local_parts: + all_local_parts.append(segment) + + # Return combined result + if all_local_parts: + return main_part + "+" + ".".join(all_local_parts) + else: + return main_part + + def format_version(version: ScmVersion) -> str: log.debug("scm version %s", version) log.debug("config %s", version.config) @@ -462,13 +536,46 @@ def format_version(version: ScmVersion) -> str: assert isinstance(version.tag, str) return version.tag + # Extract original tag's local data for later combination + original_local = "" + if hasattr(version.tag, "local") and version.tag.local is not None: + original_local = str(version.tag.local) + + # Create a patched ScmVersion with only the base version (no local data) for version schemes + from dataclasses import replace + + if version.tag: + # Extract the base version (public part) from the tag using config's version_cls + if hasattr(version.tag, "public"): + # It's a Version object with a public attribute + base_version_str = str(version.tag.public) + elif isinstance(version.tag, str): + # It's a string - strip any local part + base_version_str = version.tag.split("+")[0] + else: + # It's some other type - use string representation and strip local part + base_version_str = str(version.tag).split("+")[0] + + # Create the base tag using the config's version class + base_tag = version.config.version_cls(base_version_str) + version_for_scheme = replace(version, tag=base_tag) + else: + version_for_scheme = version + main_version = _entrypoints._call_version_scheme( - version, "setuptools_scm.version_scheme", version.config.version_scheme + version_for_scheme, + "setuptools_scm.version_scheme", + version.config.version_scheme, ) log.debug("version %s", main_version) assert main_version is not None + local_version = _entrypoints._call_version_scheme( version, "setuptools_scm.local_scheme", version.config.local_scheme, "+unknown" ) log.debug("local_version %s", local_version) - return main_version + local_version + + # Combine main version with original local data and new local scheme data + return _combine_version_with_local_parts( + str(main_version), original_local, local_version + ) diff --git a/testing/test_functions.py b/testing/test_functions.py index d6c4e711..c0cb5166 100644 --- a/testing/test_functions.py +++ b/testing/test_functions.py @@ -47,6 +47,18 @@ def test_next_tag(tag: str, expected: str) -> None: "distance-dirty": meta("1.1", distance=3, dirty=True, config=c), } +# Versions with build metadata in the tag +VERSIONS_WITH_BUILD_METADATA = { + "exact-build": meta("1.1+build.123", distance=0, dirty=False, config=c), + "dirty-build": meta("1.1+build.123", distance=0, dirty=True, config=c), + "distance-clean-build": meta("1.1+build.123", distance=3, dirty=False, config=c), + "distance-dirty-build": meta("1.1+build.123", distance=3, dirty=True, config=c), + "exact-ci": meta("2.0.0+ci.456", distance=0, dirty=False, config=c), + "dirty-ci": meta("2.0.0+ci.456", distance=0, dirty=True, config=c), + "distance-clean-ci": meta("2.0.0+ci.456", distance=2, dirty=False, config=c), + "distance-dirty-ci": meta("2.0.0+ci.456", distance=2, dirty=True, config=c), +} + @pytest.mark.parametrize( ("version", "version_scheme", "local_scheme", "expected"), @@ -77,6 +89,96 @@ def test_format_version( assert format_version(configured_version) == expected +@pytest.mark.parametrize( + ("version", "version_scheme", "local_scheme", "expected"), + [ + # Exact matches should preserve build metadata from tag + ("exact-build", "guess-next-dev", "node-and-date", "1.1+build.123"), + ("exact-build", "guess-next-dev", "no-local-version", "1.1+build.123"), + ("exact-ci", "guess-next-dev", "node-and-date", "2.0.0+ci.456"), + ("exact-ci", "guess-next-dev", "no-local-version", "2.0.0+ci.456"), + # Dirty exact matches - version scheme treats dirty as non-exact, build metadata preserved + ( + "dirty-build", + "guess-next-dev", + "node-and-date", + "1.2.dev0+build.123.d20090213", + ), + ("dirty-build", "guess-next-dev", "no-local-version", "1.2.dev0+build.123"), + ("dirty-ci", "guess-next-dev", "node-and-date", "2.0.1.dev0+ci.456.d20090213"), + # Distance cases - build metadata should be preserved and combined with SCM data + ( + "distance-clean-build", + "guess-next-dev", + "node-and-date", + "1.2.dev3+build.123", + ), + ( + "distance-clean-build", + "guess-next-dev", + "no-local-version", + "1.2.dev3+build.123", + ), + ("distance-clean-ci", "guess-next-dev", "node-and-date", "2.0.1.dev2+ci.456"), + # Distance + dirty cases - build metadata should be preserved and combined with SCM data + ( + "distance-dirty-build", + "guess-next-dev", + "node-and-date", + "1.2.dev3+build.123.d20090213", + ), + ( + "distance-dirty-ci", + "guess-next-dev", + "node-and-date", + "2.0.1.dev2+ci.456.d20090213", + ), + # Post-release scheme tests + ("exact-build", "post-release", "node-and-date", "1.1+build.123"), + ( + "dirty-build", + "post-release", + "node-and-date", + "1.1.post0+build.123.d20090213", + ), + ( + "distance-clean-build", + "post-release", + "node-and-date", + "1.1.post3+build.123", + ), + ( + "distance-dirty-build", + "post-release", + "node-and-date", + "1.1.post3+build.123.d20090213", + ), + ], +) +def test_format_version_with_build_metadata( + version: str, version_scheme: str, local_scheme: str, expected: str +) -> None: + """Test format_version with tags that contain build metadata.""" + from dataclasses import replace + + from packaging.version import Version + + scm_version = VERSIONS_WITH_BUILD_METADATA[version] + configured_version = replace( + scm_version, + config=replace( + scm_version.config, version_scheme=version_scheme, local_scheme=local_scheme + ), + ) + result = format_version(configured_version) + + # Validate result is a valid PEP 440 version + parsed = Version(result) + assert str(parsed) == result, f"Result should be valid PEP 440: {result}" + + assert result == expected, f"Expected {expected}, got {result}" + + def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None: write_to = "VERSION" version = str(VERSIONS["exact"].tag) diff --git a/testing/test_integration.py b/testing/test_integration.py index c4bfcb74..85994a54 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -3,6 +3,7 @@ import importlib.metadata import logging import os +import re import subprocess import sys import textwrap @@ -13,6 +14,8 @@ import pytest +from packaging.version import Version + import setuptools_scm._integration.setuptools if TYPE_CHECKING: @@ -381,6 +384,139 @@ def test_pretend_metadata_invalid_toml_error( assert "Failed to parse pretend metadata" in caplog.text +def test_git_tag_with_local_build_data_preserved(wd: WorkDir) -> None: + """Test that git tags containing local build data are preserved in final version.""" + wd.commit_testfile() + + # Create a git tag that includes local build data + # This simulates a CI system that creates tags with build metadata + wd("git tag 1.0.0+build.123") + + # The version should preserve the build metadata from the tag + version = wd.get_version() + + # Validate it's a proper PEP 440 version + parsed_version = Version(version) + assert str(parsed_version) == version, ( + f"Version should parse correctly as PEP 440: {version}" + ) + + # Should preserve the build metadata that was in the git tag + assert version == "1.0.0+build.123", ( + f"Expected build metadata preserved, got {version}" + ) + + # Validate the local part is correct + assert parsed_version.local == "build.123", ( + f"Expected local part 'build.123', got {parsed_version.local}" + ) + + +def test_git_tag_with_commit_hash_preserved(wd: WorkDir) -> None: + """Test that git tags with commit hash data are preserved.""" + wd.commit_testfile() + + # Create a git tag that includes commit hash metadata + wd("git tag 2.0.0+sha.abcd1234") + + # The version should preserve the commit hash from the tag + version = wd.get_version() + + # Validate it's a proper PEP 440 version + parsed_version = Version(version) + assert str(parsed_version) == version, ( + f"Version should parse correctly as PEP 440: {version}" + ) + + # Should preserve the commit hash that was in the git tag + assert version == "2.0.0+sha.abcd1234" + + # Validate the local part is correct + assert parsed_version.local == "sha.abcd1234", ( + f"Expected local part 'sha.abcd1234', got {parsed_version.local}" + ) + + +def test_git_tag_with_local_build_data_preserved_dirty_workdir(wd: WorkDir) -> None: + """Test that git tags with local build data are preserved even with dirty working directory.""" + wd.commit_testfile() + + # Create a git tag that includes local build data + wd("git tag 1.5.0+build.456") + + # Make working directory dirty + wd.write("modified_file.txt", "some changes") + + # The version should preserve the build metadata from the tag + # even when working directory is dirty + version = wd.get_version() + + # Validate it's a proper PEP 440 version + parsed_version = Version(version) + assert str(parsed_version) == version, ( + f"Version should parse correctly as PEP 440: {version}" + ) + + # Should preserve the build metadata that was in the git tag + assert version == "1.5.0+build.456", ( + f"Expected build metadata preserved with dirty workdir, got {version}" + ) + + # Validate the local part is correct + assert parsed_version.local == "build.456", ( + f"Expected local part 'build.456', got {parsed_version.local}" + ) + + +def test_git_tag_with_local_build_data_preserved_with_distance(wd: WorkDir) -> None: + """Test that git tags with local build data are preserved with distance.""" + wd.commit_testfile() + + # Create a git tag that includes local build data + wd("git tag 3.0.0+ci.789") + + # Add another commit after the tag to create distance + wd.commit_testfile("after-tag") + + # The version should use version scheme for distance but preserve original tag's build data + version = wd.get_version() + + # Validate it's a proper PEP 440 version + parsed_version = Version(version) + assert str(parsed_version) == version, ( + f"Version should parse correctly as PEP 440: {version}" + ) + + # Tag local data should be preserved and combined with SCM data + assert version.startswith("3.0.1.dev1"), ( + f"Expected dev version with distance, got {version}" + ) + + # Use regex to validate the version format with both tag build data and SCM node data + # Expected format: 3.0.1.dev1+ci.789.g + version_pattern = r"^3\.0\.1\.dev1\+ci\.789\.g[a-f0-9]+$" + assert re.match(version_pattern, version), ( + f"Version should match pattern {version_pattern}, got {version}" + ) + + # The original tag's local data (+ci.789) should be preserved and combined with SCM data + assert "+ci.789" in version, f"Tag local data should be preserved, got {version}" + + # Validate the local part contains both tag and SCM node information + assert parsed_version.local is not None, ( + f"Expected local version part, got {parsed_version.local}" + ) + assert "ci.789" in parsed_version.local, ( + f"Expected local part to contain tag data 'ci.789', got {parsed_version.local}" + ) + assert "g" in parsed_version.local, ( + f"Expected local part to contain SCM node data 'g...', got {parsed_version.local}" + ) + + # Note: This test verifies that local build data from tags is preserved and combined + # with SCM data when there's distance, which is the desired behavior for issue 1019. + + def testwarn_on_broken_setuptools() -> None: _warn_on_old_setuptools("61") with pytest.warns(RuntimeWarning, match="ERROR: setuptools==60"): From 3097c6eb20e38695f9801532358007d2a4516936 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 3 Aug 2025 17:42:41 +0200 Subject: [PATCH 080/162] first working iteration on build tag passover from scm tags to version strings addresses #1019 needs more iteration --- src/setuptools_scm/_overrides.py | 3 +- src/setuptools_scm/fallbacks.py | 3 ++ src/setuptools_scm/version.py | 50 +++++++++++++++++--------------- testing/test_basic_api.py | 16 +++++++++- testing/test_git.py | 24 ++++++++++----- testing/test_integration.py | 10 ++++--- testing/test_version.py | 34 +++++++++++++++++++++- 7 files changed, 101 insertions(+), 39 deletions(-) diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py index 698f0fa3..449cf934 100644 --- a/src/setuptools_scm/_overrides.py +++ b/src/setuptools_scm/_overrides.py @@ -164,8 +164,7 @@ def _read_pretended_version_for( pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name) if pretended: - # we use meta here since the pretended version - # must adhere to the pep to begin with + # Use enhanced meta function - let validation errors bubble up return version.meta(tag=pretended, preformatted=True, config=config) else: return None diff --git a/src/setuptools_scm/fallbacks.py b/src/setuptools_scm/fallbacks.py index 45a75351..a9a9ac0a 100644 --- a/src/setuptools_scm/fallbacks.py +++ b/src/setuptools_scm/fallbacks.py @@ -25,6 +25,7 @@ def parse_pkginfo(root: _t.PathT, config: Configuration) -> ScmVersion | None: data = data_from_mime(pkginfo) version = data.get("Version", _UNKNOWN) if version != _UNKNOWN: + # Use enhanced meta function - let validation errors bubble up return meta(version, preformatted=True, config=config) else: return None @@ -38,8 +39,10 @@ def fallback_version(root: _t.PathT, config: Configuration) -> ScmVersion | None parent_name[len(config.parentdir_prefix_version) :], config ) if version is not None: + # Use enhanced meta function - let validation errors bubble up return meta(str(version), preformatted=True, config=config) if config.fallback_version is not None: log.debug("FALLBACK %s", config.fallback_version) + # Use enhanced meta function - let validation errors bubble up return meta(config.fallback_version, preformatted=True, config=config) return None diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 6b18a092..77c26dc9 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -152,8 +152,8 @@ def _source_epoch_or_utc_now() -> datetime: class ScmVersion: """represents a parsed version from scm""" - tag: _v.Version | _v.NonNormalizedVersion | str - """the related tag or preformatted version string""" + tag: _v.Version | _v.NonNormalizedVersion + """the related tag or preformatted version""" config: _config.Configuration """the configuration used to parse the version""" distance: int = 0 @@ -223,9 +223,16 @@ def format_next_version( def _parse_tag( tag: _VersionT | str, preformatted: bool, config: _config.Configuration -) -> _VersionT | str: +) -> _VersionT: if preformatted: - return tag + # For preformatted versions, tag should already be validated as a version object + # String validation is handled in meta function before calling this + if isinstance(tag, str): + # This should not happen with enhanced meta, but kept for safety + return _v.NonNormalizedVersion(tag) + else: + # Already a version object (including test mocks), return as-is + return tag elif not isinstance(tag, config.version_cls): version = tag_to_version(tag, config) assert version is not None @@ -246,7 +253,16 @@ def meta( node_date: date | None = None, time: datetime | None = None, ) -> ScmVersion: - parsed_version = _parse_tag(tag, preformatted, config) + parsed_version: _VersionT + # Enhanced string validation for preformatted versions + if preformatted and isinstance(tag, str): + # Validate PEP 440 compliance using NonNormalizedVersion + # Let validation errors bubble up to the caller + parsed_version = _v.NonNormalizedVersion(tag) + else: + # Use existing _parse_tag logic for non-preformatted or already validated inputs + parsed_version = _parse_tag(tag, preformatted, config) + log.info("version %s -> %s", tag, parsed_version) assert parsed_version is not None, f"Can't parse version {tag}" scm_version = ScmVersion( @@ -533,8 +549,7 @@ def format_version(version: ScmVersion) -> str: log.debug("scm version %s", version) log.debug("config %s", version.config) if version.preformatted: - assert isinstance(version.tag, str) - return version.tag + return str(version.tag) # Extract original tag's local data for later combination original_local = "" @@ -544,23 +559,10 @@ def format_version(version: ScmVersion) -> str: # Create a patched ScmVersion with only the base version (no local data) for version schemes from dataclasses import replace - if version.tag: - # Extract the base version (public part) from the tag using config's version_cls - if hasattr(version.tag, "public"): - # It's a Version object with a public attribute - base_version_str = str(version.tag.public) - elif isinstance(version.tag, str): - # It's a string - strip any local part - base_version_str = version.tag.split("+")[0] - else: - # It's some other type - use string representation and strip local part - base_version_str = str(version.tag).split("+")[0] - - # Create the base tag using the config's version class - base_tag = version.config.version_cls(base_version_str) - version_for_scheme = replace(version, tag=base_tag) - else: - version_for_scheme = version + # Extract the base version (public part) from the tag using config's version_cls + base_version_str = str(version.tag.public) + base_tag = version.config.version_cls(base_version_str) + version_for_scheme = replace(version, tag=base_tag) main_version = _entrypoints._call_version_scheme( version_for_scheme, diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py index 0eb4a247..ca0c3041 100644 --- a/testing/test_basic_api.py +++ b/testing/test_basic_api.py @@ -55,7 +55,9 @@ def assert_root(monkeypatch: pytest.MonkeyPatch, expected_root: str) -> None: def assertion(config: Configuration) -> ScmVersion: assert config.absolute_root == expected_root - return ScmVersion("1.0", config=config) + from packaging.version import Version + + return ScmVersion(Version("1.0"), config=config) monkeypatch.setattr(setuptools_scm._get_version_impl, "parse_version", assertion) @@ -250,6 +252,18 @@ def __init__(self, tag_str: str) -> None: def __repr__(self) -> str: return f"hello,{self.version}" + @property + def public(self) -> str: + """The public portion of the version (without local part).""" + return self.version.split("+")[0] + + @property + def local(self) -> str | None: + """The local version segment.""" + if "+" in self.version: + return self.version.split("+", 1)[1] + return None + # you can not use normalize=False and version_cls at the same time with pytest.raises( ValueError, diff --git a/testing/test_git.py b/testing/test_git.py index 1c024027..ab105dd1 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -215,16 +215,26 @@ def test_version_from_git(wd: WorkDir) -> None: setup(use_scm_version={'normalize': False, 'write_to': 'VERSION.txt'}) """, "with_created_class": """ - from setuptools import setup +from setuptools import setup + +class MyVersion: + def __init__(self, tag_str: str): + self.version = tag_str + + def __repr__(self): + return self.version - class MyVersion: - def __init__(self, tag_str: str): - self.version = tag_str + @property + def public(self): + return self.version.split('+')[0] - def __repr__(self): - return self.version + @property + def local(self): + if '+' in self.version: + return self.version.split('+', 1)[1] + return None - setup(use_scm_version={'version_cls': MyVersion, 'write_to': 'VERSION.txt'}) +setup(use_scm_version={'version_cls': MyVersion, 'write_to': 'VERSION.txt'}) """, "with_named_import": """ from setuptools import setup diff --git a/testing/test_integration.py b/testing/test_integration.py index 85994a54..1d238791 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -193,14 +193,16 @@ def test_pretend_version_name_takes_precedence( assert wd.get_version(dist_name="test") == "1.0.0" -def test_pretend_version_accepts_bad_string( +def test_pretend_version_rejects_invalid_string( monkeypatch: pytest.MonkeyPatch, wd: WorkDir ) -> None: + """Test that invalid pretend versions raise errors and bubble up.""" monkeypatch.setenv(PRETEND_KEY, "dummy") wd.write("setup.py", SETUP_PY_PLAIN) - assert wd.get_version(write_to="test.py") == "dummy" - pyver = wd([sys.executable, "setup.py", "--version"]) - assert pyver == "0.0.0" + + # With strict validation, invalid pretend versions should raise errors + with pytest.raises(Exception, match=r".*dummy.*"): + wd.get_version(write_to="test.py") def test_pretend_metadata_with_version( diff --git a/testing/test_version.py b/testing/test_version.py index eb31cc1e..71850074 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -71,7 +71,27 @@ def test_next_semver(version: ScmVersion, expected_next: str) -> None: def test_next_semver_bad_tag() -> None: - version = meta("1.0.0-foo", preformatted=True, config=c) + # Create a mock version class that represents an invalid version for testing error handling + from typing import cast + + from setuptools_scm._version_cls import _VersionT + + class BrokenVersionForTest: + """A mock version that behaves like a string but passes type checking.""" + + def __init__(self, version_str: str): + self._version_str = version_str + + def __str__(self) -> str: + return self._version_str + + def __repr__(self) -> str: + return f"BrokenVersionForTest({self._version_str!r})" + + # Cast to the expected type to avoid type checking issues + broken_tag = cast(_VersionT, BrokenVersionForTest("1.0.0-foo")) + version = meta(broken_tag, preformatted=True, config=c) + with pytest.raises( ValueError, match=r"1\.0\.0-foo.* can't be parsed as numeric version" ): @@ -471,6 +491,18 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"MyVersion" + @property + def public(self) -> str: + """The public portion of the version (without local part).""" + return self.tag.split("+")[0] + + @property + def local(self) -> str | None: + """The local version segment.""" + if "+" in self.tag: + return self.tag.split("+", 1)[1] + return None + config = Configuration(version_cls=MyVersion) # type: ignore[arg-type] scm_version = meta("1.0.0-foo", config=config) From 3a8ec6c0eedb4b4570613ce713af129e96e2bd4b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Aug 2025 08:41:11 +0200 Subject: [PATCH 081/162] chore: unify git path strip --- src/setuptools_scm/_compat.py | 65 ++++++++++++++++++++++ src/setuptools_scm/_file_finders/git.py | 8 +-- src/setuptools_scm/git.py | 8 +-- testing/test_compat.py | 73 +++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 src/setuptools_scm/_compat.py create mode 100644 testing/test_compat.py diff --git a/src/setuptools_scm/_compat.py b/src/setuptools_scm/_compat.py new file mode 100644 index 00000000..4e9e301f --- /dev/null +++ b/src/setuptools_scm/_compat.py @@ -0,0 +1,65 @@ +"""Compatibility utilities for cross-platform functionality.""" + +from __future__ import annotations + + +def normalize_path_for_assertion(path: str) -> str: + """Normalize path separators for cross-platform assertions. + + On Windows, this converts backslashes to forward slashes to ensure + path comparisons work correctly. On other platforms, returns the path unchanged. + The length of the string is not changed by this operation. + + Args: + path: The path string to normalize + + Returns: + The path with normalized separators + """ + return path.replace("\\", "/") + + +def strip_path_suffix( + full_path: str, suffix_path: str, error_msg: str | None = None +) -> str: + """Strip a suffix from a path, with cross-platform path separator handling. + + This function first normalizes path separators for Windows compatibility, + then asserts that the full path ends with the suffix, and finally returns + the path with the suffix removed. This is the common pattern used for + computing parent directories from git output. + + Args: + full_path: The full path string + suffix_path: The suffix path to strip from the end + error_msg: Optional custom error message for the assertion + + Returns: + The prefix path with the suffix removed + + Raises: + AssertionError: If the full path doesn't end with the suffix + """ + normalized_full = normalize_path_for_assertion(full_path) + + if error_msg: + assert normalized_full.endswith(suffix_path), error_msg + else: + assert normalized_full.endswith(suffix_path), ( + f"Path assertion failed: {full_path!r} does not end with {suffix_path!r}" + ) + + return full_path[: -len(suffix_path)] + + +# Legacy aliases for backward compatibility during transition +def assert_path_endswith( + full_path: str, suffix_path: str, error_msg: str | None = None +) -> None: + """Legacy alias - use strip_path_suffix instead.""" + strip_path_suffix(full_path, suffix_path, error_msg) + + +def compute_path_prefix(full_path: str, suffix_path: str) -> str: + """Legacy alias - use strip_path_suffix instead.""" + return strip_path_suffix(full_path, suffix_path) diff --git a/src/setuptools_scm/_file_finders/git.py b/src/setuptools_scm/_file_finders/git.py index fe6dfb05..4379c21a 100644 --- a/src/setuptools_scm/_file_finders/git.py +++ b/src/setuptools_scm/_file_finders/git.py @@ -39,11 +39,9 @@ def _git_toplevel(path: str) -> str | None: # ``cwd`` is absolute path to current working directory. # the below method removes the length of ``out`` from # ``cwd``, which gives the git toplevel - assert cwd.replace("\\", "/").endswith(out), f"cwd={cwd!r}\nout={out!r}" - # In windows cwd contains ``\`` which should be replaced by ``/`` - # for this assertion to work. Length of string isn't changed by replace - # ``\\`` is just and escape for `\` - out = cwd[: -len(out)] + from .._compat import strip_path_suffix + + out = strip_path_suffix(cwd, out, f"cwd={cwd!r}\nout={out!r}") log.debug("find files toplevel %s", out) return norm_real(out) except subprocess.CalledProcessError: diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index acfd8e76..966ab69c 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -93,11 +93,9 @@ def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None: real_wd = os.fspath(wd) else: str_wd = os.fspath(wd) - assert str_wd.replace("\\", "/").endswith(real_wd) - # In windows wd contains ``\`` which should be replaced by ``/`` - # for this assertion to work. Length of string isn't changed by replace - # ``\\`` is just and escape for `\` - real_wd = str_wd[: -len(real_wd)] + from ._compat import strip_path_suffix + + real_wd = strip_path_suffix(str_wd, real_wd) log.debug("real root %s", real_wd) if not samefile(real_wd, wd): return None diff --git a/testing/test_compat.py b/testing/test_compat.py new file mode 100644 index 00000000..3cd52771 --- /dev/null +++ b/testing/test_compat.py @@ -0,0 +1,73 @@ +"""Test compatibility utilities.""" + +from __future__ import annotations + +import pytest + +from setuptools_scm._compat import normalize_path_for_assertion +from setuptools_scm._compat import strip_path_suffix + + +def test_normalize_path_for_assertion() -> None: + """Test path normalization for assertions.""" + # Unix-style paths should remain unchanged + assert normalize_path_for_assertion("/path/to/file") == "/path/to/file" + + # Windows-style paths should be normalized + assert normalize_path_for_assertion(r"C:\path\to\file") == "C:/path/to/file" + assert normalize_path_for_assertion(r"path\to\file") == "path/to/file" + + # Mixed paths should be normalized + assert normalize_path_for_assertion(r"C:\path/to\file") == "C:/path/to/file" + + # Already normalized paths should remain unchanged + assert normalize_path_for_assertion("path/to/file") == "path/to/file" + + +def test_strip_path_suffix_success() -> None: + """Test successful path suffix stripping.""" + # Unix-style paths + assert strip_path_suffix("/home/user/project", "project") == "/home/user/" + assert ( + strip_path_suffix("/home/user/project/subdir", "project/subdir") + == "/home/user/" + ) + + # Windows-style paths + assert ( + strip_path_suffix("C:\\Users\\user\\project", "project") == "C:\\Users\\user\\" + ) + assert ( + strip_path_suffix("C:\\Users\\user\\project\\subdir", "project/subdir") + == "C:\\Users\\user\\" + ) + + # Mixed paths should work due to normalization + assert ( + strip_path_suffix("C:\\Users\\user\\project", "project") == "C:\\Users\\user\\" + ) + assert strip_path_suffix("/home/user/project", "project") == "/home/user/" + + # Edge cases + assert strip_path_suffix("project", "project") == "" + assert strip_path_suffix("/project", "project") == "/" + + +def test_strip_path_suffix_failure() -> None: + """Test failed path suffix stripping.""" + with pytest.raises(AssertionError, match="Path assertion failed"): + strip_path_suffix("/home/user/project", "other") + + with pytest.raises(AssertionError, match="Custom error"): + strip_path_suffix("/home/user/project", "other", "Custom error") + + +def test_integration_example() -> None: + """Test the integration pattern used in the codebase.""" + # Simulate the pattern used in git.py and _file_finders/git.py + full_path = r"C:\\Users\\user\\project\\subdir" + suffix = "subdir" + + # Now this is a single operation + prefix = strip_path_suffix(full_path, suffix) + assert prefix == r"C:\\Users\\user\\project\\" From 00d42a7b24468624f025c9c0a26cba2a7bca703f Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Aug 2025 08:43:27 +0200 Subject: [PATCH 082/162] remove ai slop in comments --- CHANGELOG.md | 4 ++++ src/setuptools_scm/_overrides.py | 1 - src/setuptools_scm/fallbacks.py | 3 --- testing/test_cli.py | 2 -- testing/test_integration.py | 10 +--------- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d6d4ef3..10d46481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking + +- fix #1019: pass python version build tags from scm version to results propperly + ### Added - add `setuptools-scm` console_scripts entry point to make the CLI directly executable diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py index 449cf934..5621534f 100644 --- a/src/setuptools_scm/_overrides.py +++ b/src/setuptools_scm/_overrides.py @@ -164,7 +164,6 @@ def _read_pretended_version_for( pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name) if pretended: - # Use enhanced meta function - let validation errors bubble up return version.meta(tag=pretended, preformatted=True, config=config) else: return None diff --git a/src/setuptools_scm/fallbacks.py b/src/setuptools_scm/fallbacks.py index a9a9ac0a..45a75351 100644 --- a/src/setuptools_scm/fallbacks.py +++ b/src/setuptools_scm/fallbacks.py @@ -25,7 +25,6 @@ def parse_pkginfo(root: _t.PathT, config: Configuration) -> ScmVersion | None: data = data_from_mime(pkginfo) version = data.get("Version", _UNKNOWN) if version != _UNKNOWN: - # Use enhanced meta function - let validation errors bubble up return meta(version, preformatted=True, config=config) else: return None @@ -39,10 +38,8 @@ def fallback_version(root: _t.PathT, config: Configuration) -> ScmVersion | None parent_name[len(config.parentdir_prefix_version) :], config ) if version is not None: - # Use enhanced meta function - let validation errors bubble up return meta(str(version), preformatted=True, config=config) if config.fallback_version is not None: log.debug("FALLBACK %s", config.fallback_version) - # Use enhanced meta function - let validation errors bubble up return meta(config.fallback_version, preformatted=True, config=config) return None diff --git a/testing/test_cli.py b/testing/test_cli.py index 480793c5..ffdcebd2 100644 --- a/testing/test_cli.py +++ b/testing/test_cli.py @@ -93,7 +93,6 @@ def test_cli_create_archival_file_stable( archival_file = wd.cwd / ".git_archival.txt" assert not archival_file.exists() - # Test successful creation result = main(["create-archival-file", "--stable"]) assert result == 0 assert archival_file.exists() @@ -122,7 +121,6 @@ def test_cli_create_archival_file_full( archival_file = wd.cwd / ".git_archival.txt" assert not archival_file.exists() - # Test successful creation result = main(["create-archival-file", "--full"]) assert result == 0 assert archival_file.exists() diff --git a/testing/test_integration.py b/testing/test_integration.py index 1d238791..074785f9 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -219,8 +219,7 @@ def test_pretend_metadata_with_version( # 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 + wd("mkdir -p src") version_file_content = """ version = '{version}' major = {version_tuple[0]} @@ -234,7 +233,6 @@ def test_pretend_metadata_with_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 @@ -266,10 +264,8 @@ def test_pretend_metadata_without_version_warns( # 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 @@ -301,7 +297,6 @@ def test_pretend_metadata_with_scm_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.short_node}' @@ -403,7 +398,6 @@ def test_git_tag_with_local_build_data_preserved(wd: WorkDir) -> None: f"Version should parse correctly as PEP 440: {version}" ) - # Should preserve the build metadata that was in the git tag assert version == "1.0.0+build.123", ( f"Expected build metadata preserved, got {version}" ) @@ -430,7 +424,6 @@ def test_git_tag_with_commit_hash_preserved(wd: WorkDir) -> None: f"Version should parse correctly as PEP 440: {version}" ) - # Should preserve the commit hash that was in the git tag assert version == "2.0.0+sha.abcd1234" # Validate the local part is correct @@ -459,7 +452,6 @@ def test_git_tag_with_local_build_data_preserved_dirty_workdir(wd: WorkDir) -> N f"Version should parse correctly as PEP 440: {version}" ) - # Should preserve the build metadata that was in the git tag assert version == "1.5.0+build.456", ( f"Expected build metadata preserved with dirty workdir, got {version}" ) From 9aa1d75d4c8a699bd4381a66ca2893ef3bf557bf Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Aug 2025 09:23:53 +0200 Subject: [PATCH 083/162] chore: use DIST_ANME in docs - easier to read than NORMALIZED_DIST_NAME - addresses part of #986 --- docs/config.md | 2 +- docs/integrations.md | 8 ++++---- docs/overrides.md | 6 +++--- docs/usage.md | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/config.md b/docs/config.md index e4a77ef0..9a46f607 100644 --- a/docs/config.md +++ b/docs/config.md @@ -157,7 +157,7 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ it is strongly recommended to use distribution-specific pretend versions (see below). -`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}` +`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` : used as the primary source for the version number, in which case it will be an unparsed string. Specifying distribution-specific pretend versions will diff --git a/docs/integrations.md b/docs/integrations.md index e94a6ea0..69b18486 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -47,7 +47,7 @@ build: - export SETUPTOOLS_SCM_OVERRIDES_FOR_${READTHEDOCS_PROJECT//-/_}='{scm.git.pre_parse="fail_on_shallow"}' ``` -This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow. +This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow. ## CI/CD and Package Publishing @@ -67,7 +67,7 @@ These local version components (`+g1a2b3c4d5`, `+dirty`) prevent uploading to Py #### The Solution -Use the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `local_scheme` to `no-local-version` when building for upload to PyPI. +Use the `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` environment variable to override the `local_scheme` to `no-local-version` when building for upload to PyPI. ### GitHub Actions Example @@ -287,9 +287,9 @@ publish-release: #### Environment Variable Format -The environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` must be set where: +The environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` must be set where: -1. **`${NORMALIZED_DIST_NAME}`** is your package name normalized according to PEP 503: +1. **`${DIST_NAME}`** is your package name normalized according to PEP 503: - Convert to uppercase - Replace hyphens and dots with underscores - Examples: `my-package` → `MY_PACKAGE`, `my.package` → `MY_PACKAGE` diff --git a/docs/overrides.md b/docs/overrides.md index 1a0f2d54..4d136db2 100644 --- a/docs/overrides.md +++ b/docs/overrides.md @@ -7,7 +7,7 @@ setuptools-scm provides a mechanism to override the version number build time. the environment variable `SETUPTOOLS_SCM_PRETEND_VERSION` is used 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}` +to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` where the dist name normalization follows adapted PEP 503 semantics. ## pretend metadata @@ -17,7 +17,7 @@ setuptools-scm provides a mechanism to override individual version metadata fiel 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}` +To be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${DIST_NAME}` where the dist name normalization follows adapted PEP 503 semantics. ### Supported fields @@ -82,7 +82,7 @@ export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}' ## config overrides -setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` +setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` as a toml inline map to override the configuration data from `pyproject.toml`. ## subprocess timeouts diff --git a/docs/usage.md b/docs/usage.md index efa922cb..7938b4e6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -221,7 +221,7 @@ Note that running this Dockerfile requires docker with BuildKit enabled To avoid BuildKit and mounting of the .git folder altogether, one can also pass the desired version as a build argument. -Note that `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}` +Note that `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` is preferred over `SETUPTOOLS_SCM_PRETEND_VERSION`. From becaeb4c75552c8241dc861b085d0c91fb04d595 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Aug 2025 10:20:31 +0200 Subject: [PATCH 084/162] hardend environment override finding - match non-normalized suffixes - show close matches on miss for typos --- src/setuptools_scm/_overrides.py | 140 +++++++++++++++++- testing/test_overrides.py | 246 +++++++++++++++++++++++++++++++ 2 files changed, 378 insertions(+), 8 deletions(-) create mode 100644 testing/test_overrides.py diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py index 5621534f..4e06b7a7 100644 --- a/src/setuptools_scm/_overrides.py +++ b/src/setuptools_scm/_overrides.py @@ -2,9 +2,12 @@ import dataclasses import os -import re +from difflib import get_close_matches from typing import Any +from typing import Mapping + +from packaging.utils import canonicalize_name from . import _config from . import _log @@ -19,18 +22,139 @@ PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}" +def _search_env_vars_with_prefix( + prefix: str, dist_name: str, env: Mapping[str, str] +) -> list[tuple[str, str]]: + """Search environment variables with a given prefix for potential dist name matches. + + Args: + prefix: The environment variable prefix (e.g., "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_") + dist_name: The original dist name to match against + env: Environment dictionary to search in + + Returns: + List of (env_var_name, env_var_value) tuples for potential matches + """ + # Get the canonical name for comparison + canonical_dist_name = canonicalize_name(dist_name) + + matches = [] + for env_var, value in env.items(): + if env_var.startswith(prefix): + suffix = env_var[len(prefix) :] + # Normalize the suffix and compare to canonical dist name + try: + normalized_suffix = canonicalize_name(suffix.lower().replace("_", "-")) + if normalized_suffix == canonical_dist_name: + matches.append((env_var, value)) + except Exception: + # If normalization fails for any reason, skip this env var + continue + + return matches + + +def _find_close_env_var_matches( + prefix: str, expected_suffix: str, env: Mapping[str, str], threshold: float = 0.6 +) -> list[str]: + """Find environment variables with similar suffixes that might be typos. + + Args: + prefix: The environment variable prefix + expected_suffix: The expected suffix (canonicalized dist name in env var format) + env: Environment dictionary to search in + threshold: Similarity threshold for matches (0.0 to 1.0) + + Returns: + List of environment variable names that are close matches + """ + candidates = [] + for env_var in env: + if env_var.startswith(prefix): + suffix = env_var[len(prefix) :] + candidates.append(suffix) + + # Use difflib to find close matches + close_matches = get_close_matches( + expected_suffix, candidates, n=3, cutoff=threshold + ) + + return [f"{prefix}{match}" for match in close_matches if match != expected_suffix] + + def read_named_env( - *, tool: str = "SETUPTOOLS_SCM", name: str, dist_name: str | None + *, + tool: str = "SETUPTOOLS_SCM", + name: str, + dist_name: str | None, + env: Mapping[str, str] = os.environ, ) -> str | None: - """ """ + """Read a named environment variable, with fallback search for dist-specific variants. + + This function first tries the standard normalized environment variable name. + If that's not found and a dist_name is provided, it searches for alternative + normalizations and warns about potential issues. + + Args: + tool: The tool prefix (default: "SETUPTOOLS_SCM") + name: The environment variable name component + dist_name: The distribution name for dist-specific variables + env: Environment dictionary to search in (defaults to os.environ) + + Returns: + The environment variable value if found, None otherwise + """ + + # First try the generic version + generic_val = env.get(f"{tool}_{name}") + if dist_name is not None: - # Normalize the dist name as per PEP 503. - normalized_dist_name = re.sub(r"[-_.]+", "-", dist_name) - env_var_dist_name = normalized_dist_name.replace("-", "_").upper() - val = os.environ.get(f"{tool}_{name}_FOR_{env_var_dist_name}") + # Normalize the dist name using packaging.utils.canonicalize_name + canonical_dist_name = canonicalize_name(dist_name) + env_var_dist_name = canonical_dist_name.replace("-", "_").upper() + expected_env_var = f"{tool}_{name}_FOR_{env_var_dist_name}" + + # Try the standard normalized name first + val = env.get(expected_env_var) if val is not None: return val - return os.environ.get(f"{tool}_{name}") + + # If not found, search for alternative normalizations + prefix = f"{tool}_{name}_FOR_" + alternative_matches = _search_env_vars_with_prefix(prefix, dist_name, env) + + if alternative_matches: + # Found alternative matches - use the first one but warn + env_var, value = alternative_matches[0] + log.warning( + "Found environment variable '%s' for dist name '%s', " + "but expected '%s'. Consider using the standard normalized name.", + env_var, + dist_name, + expected_env_var, + ) + if len(alternative_matches) > 1: + other_vars = [var for var, _ in alternative_matches[1:]] + log.warning( + "Multiple alternative environment variables found: %s. Using '%s'.", + other_vars, + env_var, + ) + return value + + # No exact or alternative matches found - look for potential typos + close_matches = _find_close_env_var_matches(prefix, env_var_dist_name, env) + if close_matches: + log.warning( + "Environment variable '%s' not found for dist name '%s' " + "(canonicalized as '%s'). Did you mean one of these? %s", + expected_env_var, + dist_name, + canonical_dist_name, + close_matches, + ) + + return generic_val def _read_pretended_metadata_for( diff --git a/testing/test_overrides.py b/testing/test_overrides.py new file mode 100644 index 00000000..afba5339 --- /dev/null +++ b/testing/test_overrides.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +import logging + +import pytest + +from setuptools_scm._overrides import _find_close_env_var_matches +from setuptools_scm._overrides import _search_env_vars_with_prefix +from setuptools_scm._overrides import read_named_env + + +class TestSearchEnvVarsWithPrefix: + """Test the _search_env_vars_with_prefix helper function.""" + + def test_exact_match(self) -> None: + """Test finding exact normalized matches.""" + env = {"SETUPTOOLS_SCM_TEST_FOR_MY_PACKAGE": "value1"} + + matches = _search_env_vars_with_prefix( + "SETUPTOOLS_SCM_TEST_FOR_", "my-package", env + ) + + assert len(matches) == 1 + assert matches[0] == ("SETUPTOOLS_SCM_TEST_FOR_MY_PACKAGE", "value1") + + def test_multiple_normalizations(self) -> None: + """Test finding various normalization patterns.""" + # Set up different normalization patterns + env = { + "SETUPTOOLS_SCM_TEST_FOR_MY_AWESOME_PKG": "value1", + "SETUPTOOLS_SCM_TEST_FOR_MYAWESOMEPKG": "value2", + "SETUPTOOLS_SCM_TEST_FOR_MY_AWESOME-PKG": "value3", # duplicate + } + + matches = _search_env_vars_with_prefix( + "SETUPTOOLS_SCM_TEST_FOR_", "my-awesome.pkg", env + ) + + # Should find the variants that match our normalization patterns + assert len(matches) >= 1 + env_vars = [var for var, _ in matches] + assert "SETUPTOOLS_SCM_TEST_FOR_MY_AWESOME_PKG" in env_vars + + def test_no_matches(self) -> None: + """Test when no matches are found.""" + # Set up unrelated env vars + env = { + "OTHER_VAR": "value", + "SETUPTOOLS_SCM_OTHER_FOR_SOMETHING": "value", + } + + matches = _search_env_vars_with_prefix( + "SETUPTOOLS_SCM_TEST_FOR_", "nonexistent", env + ) + + assert len(matches) == 0 + + def test_case_variations(self) -> None: + """Test that case variations are handled.""" + env = {"SETUPTOOLS_SCM_TEST_FOR_MYPACKAGE": "value1"} + + matches = _search_env_vars_with_prefix( + "SETUPTOOLS_SCM_TEST_FOR_", "MyPackage", env + ) + + assert len(matches) == 1 + assert matches[0][1] == "value1" + + +class TestFindCloseEnvVarMatches: + """Test the _find_close_env_var_matches helper function.""" + + def test_close_matches(self) -> None: + """Test finding close matches for potential typos.""" + env = { + "SETUPTOOLS_SCM_TEST_FOR_MY_PACKAG": "typo1", # missing 'e' + "SETUPTOOLS_SCM_TEST_FOR_MY_PAKAGE": "typo2", # 'c' -> 'k' + "SETUPTOOLS_SCM_TEST_FOR_OTHER_PKG": "unrelated", + } + + close_matches = _find_close_env_var_matches( + "SETUPTOOLS_SCM_TEST_FOR_", "MY_PACKAGE", env + ) + + # Should find the close matches but not the unrelated one + assert "SETUPTOOLS_SCM_TEST_FOR_MY_PACKAG" in close_matches + assert "SETUPTOOLS_SCM_TEST_FOR_MY_PAKAGE" in close_matches + assert "SETUPTOOLS_SCM_TEST_FOR_OTHER_PKG" not in close_matches + + def test_threshold(self) -> None: + """Test that threshold filtering works.""" + env = {"SETUPTOOLS_SCM_TEST_FOR_COMPLETELY_DIFFERENT": "unrelated"} + + close_matches = _find_close_env_var_matches( + "SETUPTOOLS_SCM_TEST_FOR_", "MY_PACKAGE", env, threshold=0.8 + ) + + # With high threshold, completely different string shouldn't match + assert len(close_matches) == 0 + + def test_no_close_matches(self) -> None: + """Test when no close matches exist.""" + env: dict[str, str] = {} + close_matches = _find_close_env_var_matches( + "SETUPTOOLS_SCM_TEST_FOR_", "MY_PACKAGE", env + ) + + assert len(close_matches) == 0 + + +class TestReadNamedEnvEnhanced: + """Test the enhanced read_named_env function.""" + + def test_standard_behavior_unchanged(self) -> None: + """Test that standard behavior still works.""" + # Generic env var + env = {"SETUPTOOLS_SCM_TEST": "generic_value"} + assert read_named_env(name="TEST", dist_name=None, env=env) == "generic_value" + + # Dist-specific env var (standard normalization) + env = {"SETUPTOOLS_SCM_TEST_FOR_MY_PACKAGE": "specific_value"} + assert ( + read_named_env(name="TEST", dist_name="my-package", env=env) + == "specific_value" + ) + + def test_alternative_normalization_found( + self, caplog: pytest.LogCaptureFixture + ) -> None: + """Test finding alternative normalizations with warnings.""" + # Set up an alternative normalization pattern (user uses dots instead of canonical hyphens) + env = {"SETUPTOOLS_SCM_TEST_FOR_MY.PACKAGE": "alt_value"} + + with caplog.at_level(logging.WARNING): + result = read_named_env(name="TEST", dist_name="my.package", env=env) + + assert result == "alt_value" + assert "Found environment variable" in caplog.text + assert "but expected" in caplog.text + + def test_multiple_alternatives_warning( + self, caplog: pytest.LogCaptureFixture + ) -> None: + """Test warning when multiple alternative normalizations exist.""" + # Set up multiple alternatives that represent the same canonical package name + # but use different normalizations in the env var + env = { + "SETUPTOOLS_SCM_TEST_FOR_MY.PACKAGE": "alt1", # dots instead of hyphens + "SETUPTOOLS_SCM_TEST_FOR_MY-PACKAGE": "alt2", # dashes instead of underscores + "SETUPTOOLS_SCM_TEST_FOR_my.package": "alt3", # lowercase + } + + with caplog.at_level(logging.WARNING): + result = read_named_env(name="TEST", dist_name="my.package", env=env) + + assert result in ["alt1", "alt2", "alt3"] # Should use one of them + assert "Multiple alternative environment variables found" in caplog.text + + def test_typo_suggestions(self, caplog: pytest.LogCaptureFixture) -> None: + """Test suggestions for potential typos.""" + # Set up a close but not exact match + env = {"SETUPTOOLS_SCM_TEST_FOR_MY_PACKAG": "typo_value"} + + with caplog.at_level(logging.WARNING): + result = read_named_env(name="TEST", dist_name="my-package", env=env) + + # Should return None (generic fallback) but warn about close matches + assert result is None + assert "Did you mean one of these?" in caplog.text + assert "SETUPTOOLS_SCM_TEST_FOR_MY_PACKAG" in caplog.text + + def test_fallback_to_generic(self) -> None: + """Test fallback to generic env var when dist-specific not found.""" + env = {"SETUPTOOLS_SCM_TEST": "generic_fallback"} + + result = read_named_env(name="TEST", dist_name="nonexistent-package", env=env) + + assert result == "generic_fallback" + + def test_no_generic_fallback(self) -> None: + """Test behavior when neither dist-specific nor generic env vars exist.""" + env: dict[str, str] = {} + result = read_named_env(name="TEST", dist_name="some-package", env=env) + + assert result is None + + def test_dist_specific_overrides_generic(self) -> None: + """Test that dist-specific env vars override generic ones.""" + env = { + "SETUPTOOLS_SCM_TEST": "generic", + "SETUPTOOLS_SCM_TEST_FOR_MY_PACKAGE": "specific", + } + + result = read_named_env(name="TEST", dist_name="my-package", env=env) + + assert result == "specific" + + def test_custom_tool_prefix(self) -> None: + """Test that custom tool prefixes work.""" + env = {"CUSTOM_TOOL_TEST_FOR_MY_PACKAGE": "custom_value"} + + result = read_named_env( + tool="CUSTOM_TOOL", name="TEST", dist_name="my-package", env=env + ) + + assert result == "custom_value" + + def test_complex_dist_name_normalization( + self, caplog: pytest.LogCaptureFixture + ) -> None: + """Test complex dist name normalization scenarios.""" + # User uses a non-canonical format (keeping underscores instead of canonical hyphens) + # The canonical form of "complex.dist-name_with.dots" is "complex-dist-name-with-dots" + # which becomes "COMPLEX_DIST_NAME_WITH_DOTS" as env var + # But user set it with mixed format: + env = {"SETUPTOOLS_SCM_TEST_FOR_COMPLEX.DIST_NAME_WITH.DOTS": "value"} + + with caplog.at_level(logging.WARNING): + result = read_named_env( + name="TEST", dist_name="complex.dist-name_with.dots", env=env + ) + + assert result == "value" + assert "Found environment variable" in caplog.text + + def test_lowercase_environment_variable( + self, caplog: pytest.LogCaptureFixture + ) -> None: + """Test that lowercase environment variables are found as alternatives.""" + env = {"SETUPTOOLS_SCM_TEST_FOR_my.package": "lowercase_value"} + + with caplog.at_level(logging.WARNING): + result = read_named_env(name="TEST", dist_name="my.package", env=env) + + assert result == "lowercase_value" + assert "Found environment variable" in caplog.text + assert "but expected" in caplog.text + + def test_edge_case_empty_dist_name(self) -> None: + """Test edge case with empty dist name.""" + env = {"SETUPTOOLS_SCM_TEST": "generic"} + + result = read_named_env(name="TEST", dist_name="", env=env) + + # Should still try dist-specific lookup but fall back to generic + assert result == "generic" From 6a002e0dd32b00888359b9c7c4ea4b09b127de16 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Aug 2025 10:50:44 +0200 Subject: [PATCH 085/162] more consistent documentation on setuptools version requirements and recommendations closes #921 --- CHANGELOG.md | 1 + README.md | 12 +++++++++--- docs/customizing.md | 2 +- docs/examples/version_scheme_code/pyproject.toml | 2 +- docs/index.md | 5 +++-- docs/usage.md | 9 +++++++-- src/setuptools_scm/_integration/setuptools.py | 2 +- 7 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10d46481..c33f0ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - fix #1150: enable setuptools-scm when we are a build requirement - feature #1154: add the commit id the the default version file template - drop scriv +- fix #921: document setuptools version requirements more consistently - 61 as minumum asn 8 as recommended minimum ### Fixed diff --git a/README.md b/README.md index 7d9c056a..f4ca4bf9 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ or [configuring Git archive][git-archive-docs]. The preferred way to configure [setuptools-scm] is to author settings in a `tool.setuptools_scm` section of `pyproject.toml`. -This feature requires setuptools 61 or later. +This feature requires setuptools 61 or later (recommended: >=80 for best compatibility). First, ensure that [setuptools-scm] is present during the project's build step by specifying it as one of the build requirements. ```toml title="pyproject.toml" [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" ``` @@ -61,7 +61,7 @@ dynamic = ["version"] ```toml title="pyproject.toml" [build-system] - requires = ["setuptools>=64", "setuptools-scm>=8"] + requires = ["setuptools>=80", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] @@ -108,6 +108,12 @@ modern [setuptools-scm] is unable to support them sensibly. It's strongly recommended to build a wheel artifact using modern Python and setuptools, then installing the artifact instead of trying to run against old setuptools versions. +!!! note "Legacy Setuptools Support" + While setuptools-scm recommends setuptools >=80, it maintains compatibility with setuptools 61+ + to support legacy deployments that cannot easily upgrade. Support for setuptools <80 is deprecated + and will be removed in a future release. This allows enterprise environments and older CI/CD systems + to continue using setuptools-scm while still encouraging adoption of newer versions. + ## Code of Conduct diff --git a/docs/customizing.md b/docs/customizing.md index 577da0cd..18ee8765 100644 --- a/docs/customizing.md +++ b/docs/customizing.md @@ -33,7 +33,7 @@ setup(use_scm_version={"version_scheme": myversion_func}) ``` { .toml title="pyproject.toml" file="docs/examples/version_scheme_code/pyproject.toml" } [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm>=8"] # setuptools>=61 minimum, >=80 recommended build-backend = "setuptools.build_meta" [project] diff --git a/docs/examples/version_scheme_code/pyproject.toml b/docs/examples/version_scheme_code/pyproject.toml index 389aad09..a30582fe 100644 --- a/docs/examples/version_scheme_code/pyproject.toml +++ b/docs/examples/version_scheme_code/pyproject.toml @@ -1,6 +1,6 @@ # ~/~ begin <>[init] [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm>=8"] # setuptools>=61 minimum, >=80 recommended build-backend = "setuptools.build_meta" [project] diff --git a/docs/index.md b/docs/index.md index b03291e3..303017f3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,12 +27,13 @@ or [configuring Git archive][git-archive-docs]. ### With setuptools Note: `setuptools-scm>=8` intentionally doesn't depend on setuptools to ease non-setuptools usage. -Please ensure a recent version of setuptools (>=64) is installed. +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. ```toml title="pyproject.toml" [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] diff --git a/docs/usage.md b/docs/usage.md index 7938b4e6..9924ed49 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,6 +2,11 @@ ## At build time +!!! note "Setuptools Version Requirements" + setuptools-scm requires setuptools 61 or later (minimum), but recommends >=80 for best compatibility. + 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: ### Automatic Configuration (Recommended for Simple Cases) @@ -11,7 +16,7 @@ in your build requirements: ```toml title="pyproject.toml" [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] @@ -29,7 +34,7 @@ If you need to customize setuptools-scm behavior, use the `tool.setuptools_scm` ```toml title="pyproject.toml" [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 242f4152..e96807e4 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -34,7 +34,7 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: ERROR: setuptools=={_version} is used in combination with setuptools-scm>=8.x Your build configuration is incomplete and previously worked by accident! -setuptools-scm requires setuptools>=61 +setuptools-scm requires setuptools>=61 (recommended: >=80) Suggested workaround if applicable: - migrating from the deprecated setup_requires mechanism to pep517/518 From 9169ecec6a542ef343674045ba912ef78c86cb07 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Aug 2025 10:59:51 +0200 Subject: [PATCH 086/162] document fallback root in config/api closes #554 --- docs/config.md | 10 ++++++++++ docs/usage.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/docs/config.md b/docs/config.md index 9a46f607..80ad24fd 100644 --- a/docs/config.md +++ b/docs/config.md @@ -94,6 +94,16 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ unset (the default), `setuptools-scm` will error if it fails to detect the version. +`fallback_root: Path | PathLike[str] = "."` +: The directory to use when SCM metadata is not available (e.g., in extracted + archives like PyPI tarballs). This is particularly useful for legacy + configurations that need to work both in development (with SCM metadata) + and from archives (without SCM metadata). Defaults to the current directory. + + When SCM metadata is present, the `root` parameter is used; when it's not + available, `fallback_root` is used instead. This allows the same configuration + to work in both scenarios without modification. + `parse: Callable[[Path, Config], ScmVersion] | None = None` : A function that will be used instead of the discovered SCM for parsing the version. Use with caution, diff --git a/docs/usage.md b/docs/usage.md index 9924ed49..28e0bc52 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -178,8 +178,24 @@ from setuptools_scm import get_version version = get_version(root='..', relative_to=__file__) ``` +For legacy configurations or when working with extracted archives (like PyPI tarballs), +you may need to specify a `fallback_root` parameter. This is particularly useful +for legacy Sphinx configurations that use `get_version()` instead of getting the +version from the installed package: + +```python +from setuptools_scm import get_version +# For legacy Sphinx conf.py that needs to work both in development and from archives +version = get_version(root='..', fallback_root='..', relative_to=__file__) +``` + +The `fallback_root` parameter specifies the directory to use when the SCM metadata +is not available (e.g., in extracted tarballs), while `root` is used when SCM +metadata is present. + ### Usage from Sphinx +The recommended approach for Sphinx configurations is to use the installed package metadata: ``` {.python file=docs/.entangled/sphinx_conf.py} from importlib.metadata import version as get_version @@ -192,6 +208,21 @@ The underlying reason is that services like *Read the Docs* sometimes change the working directory for good reasons and using the installed metadata prevents using needless volatile data there. +!!! note "Legacy Sphinx configurations" + + If you have a legacy Sphinx configuration that still uses `setuptools_scm.get_version()` + directly (instead of `importlib.metadata`), you may need to use the `fallback_root` + parameter to ensure it works both in development and when building from archives: + + ```python + from setuptools_scm import get_version + # Legacy approach - use fallback_root for archive compatibility + release = get_version(root='..', fallback_root='..', relative_to=__file__) + version = ".".join(release.split('.')[:2]) + ``` + + However, it's strongly recommended to migrate to the `importlib.metadata` approach above. + ### With Docker/Podman From dd0f9a5187e91f88242b9a005387cef0af395b7b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Aug 2025 11:12:36 +0200 Subject: [PATCH 087/162] update changelog for the 9.0.0 release --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c33f0ab4..3b28221d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## v9.0.0 ### Breaking @@ -16,6 +16,7 @@ - fix #987: expand documentation on git archival files and add cli tools for good defaults - fix #311: document github/gitlab ci pipelines that enable auto-upload to test-pypi/pypi - fix #1022: allow `version_keyword` to override `infer_version` when configuration differs +- fix #554: document `fallback_root` parameter in programmatic usage and configuration ### Changed @@ -53,6 +54,10 @@ - fix #873: don't infer version in cli if --no-version is given - fix #535: accept tags from a release action in the gh ui - fix #1073: explain namespaces for release-branch-semver +- fix #1052: use consistent node hash length across all SCM backends +- fix #1045: reindent the `__all__` in the version template for better readability +- fix #968: harden environment override finding with better normalization and typo suggestions +- fix #846: add support for failing on missing submodules ## v8.3.1 From a921c02aa8be7545527ac1528f7fc6bd5f94aecb Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Aug 2025 14:03:11 +0200 Subject: [PATCH 088/162] correctly handle scm_version=None in version dumping closes #1180 --- CHANGELOG.md | 8 +++ .../_integration/dump_version.py | 31 +++++++--- testing/test_functions.py | 58 +++++++++++++++++++ 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b28221d..1b952ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog + +## v9.0.1 + +### Fixed + +- fix #1180: ensure version dumping works when no scm_version is given (problems in downstreams) + + ## v9.0.0 ### Breaking diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py index 62827a1b..06081c9f 100644 --- a/src/setuptools_scm/_integration/dump_version.py +++ b/src/setuptools_scm/_integration/dump_version.py @@ -95,19 +95,34 @@ def _validate_template(target: Path, template: str | None) -> str: return template +class DummyScmVersion: + @property + def short_node(self) -> str | None: + return None + + def write_version_to_path( - target: Path, template: str | None, version: str, scm_version: ScmVersion | None + target: Path, + template: str | None, + version: str, + scm_version: ScmVersion | None = None, ) -> None: final_template = _validate_template(target, template) log.debug("dump %s into %s", version, target) version_tuple = _version_as_tuple(version) - if scm_version is not None: - content = final_template.format( - version=version, - version_tuple=version_tuple, - scm_version=scm_version, + if scm_version is None: + warnings.warn( + "write_version_to_path called without scm_version parameter. " + "This will be required in a future version. " + "Pass scm_version=None explicitly to suppress this warning.", + DeprecationWarning, + stacklevel=2, ) - else: - content = final_template.format(version=version, version_tuple=version_tuple) + + content = final_template.format( + version=version, + version_tuple=version_tuple, + scm_version=scm_version or DummyScmVersion(), + ) target.write_text(content, encoding="utf-8") diff --git a/testing/test_functions.py b/testing/test_functions.py index c0cb5166..b6b8a59e 100644 --- a/testing/test_functions.py +++ b/testing/test_functions.py @@ -293,3 +293,61 @@ def test_has_command_logs_stderr(caplog: pytest.LogCaptureFixture) -> None: def test_tag_to_version(tag: str, expected_version: str) -> None: version = str(tag_to_version(tag, c)) assert version == expected_version + + +def test_write_version_to_path_deprecation_warning_none(tmp_path: Path) -> None: + """Test that write_version_to_path warns when scm_version=None is passed.""" + from setuptools_scm._integration.dump_version import write_version_to_path + + target_file = tmp_path / "version.py" + + # This should raise a deprecation warning when scm_version=None is explicitly passed + with pytest.warns( + DeprecationWarning, match="write_version_to_path called without scm_version" + ): + write_version_to_path( + target=target_file, + template=None, # Use default template + version="1.2.3", + scm_version=None, # Explicitly passing None should warn + ) + + # Verify the file was created and contains the expected content + assert target_file.exists() + content = target_file.read_text(encoding="utf-8") + + # Check that the version is correctly formatted + assert "__version__ = version = '1.2.3'" in content + assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content + + # Check that commit_id is set to None when scm_version is None + assert "__commit_id__ = commit_id = None" in content + + +def test_write_version_to_path_deprecation_warning_missing(tmp_path: Path) -> None: + """Test that write_version_to_path warns when scm_version parameter is not provided.""" + from setuptools_scm._integration.dump_version import write_version_to_path + + target_file = tmp_path / "version.py" + + # This should raise a deprecation warning when scm_version is not provided + with pytest.warns( + DeprecationWarning, match="write_version_to_path called without scm_version" + ): + write_version_to_path( + target=target_file, + template=None, # Use default template + version="1.2.3", + # scm_version not provided - should warn + ) + + # Verify the file was created and contains the expected content + assert target_file.exists() + content = target_file.read_text(encoding="utf-8") + + # Check that the version is correctly formatted + assert "__version__ = version = '1.2.3'" in content + assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content + + # Check that commit_id is set to None when scm_version is None + assert "__commit_id__ = commit_id = None" in content From 4ffcbfc725ad768899c55c0cb3e0e87b9d767749 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Aug 2025 16:47:03 +0200 Subject: [PATCH 089/162] introduce a workaround for not walking the dependency graph whne figuring if we should enable closes #1181 --- CHANGELOG.md | 3 +- src/setuptools_scm/_config.py | 13 +++++++- .../_integration/pyproject_reading.py | 3 +- src/setuptools_scm/_integration/setuptools.py | 1 + testing/test_integration.py | 32 +++++++++++++++++++ 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b952ade..ed1f30c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ ### Fixed - fix #1180: ensure version dumping works when no scm_version is given (problems in downstreams) - +- fix #1181: config - reintroduce controll over when we expect a section to be present + as it turns out theres valid use cases where setuptools_scm is not direct part of the dependencies ## v9.0.0 diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 0387de9d..c4ebc773 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -271,6 +271,7 @@ def from_file( name: str | os.PathLike[str] = "pyproject.toml", dist_name: str | None = None, missing_file_ok: bool = False, + missing_section_ok: bool = False, **kwargs: Any, ) -> Configuration: """ @@ -279,10 +280,20 @@ def from_file( 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). + + 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 """ try: - pyproject_data = _read_pyproject(Path(name)) + pyproject_data = _read_pyproject( + Path(name), missing_section_ok=missing_section_ok + ) except FileNotFoundError: if missing_file_ok: log.warning("File %s not found, using empty configuration", name) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index baf850d1..8e59ce4e 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -47,6 +47,7 @@ def read_pyproject( path: Path = Path("pyproject.toml"), tool_name: str = "setuptools_scm", build_package_names: Sequence[str] = ("setuptools_scm", "setuptools-scm"), + missing_section_ok: bool = False, ) -> PyProjectData: defn = read_toml_content(path) requires: list[str] = defn.get("build-system", {}).get("requires", []) @@ -55,7 +56,7 @@ def read_pyproject( try: section = defn.get("tool", {})[tool_name] except LookupError as e: - if not is_required: + 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. " diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index e96807e4..e5ff73e5 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -130,6 +130,7 @@ def version_keyword( config = _config.Configuration.from_file( dist_name=dist_name, missing_file_ok=True, + missing_section_ok=True, **overrides, ) _assign_version(dist, config) diff --git a/testing/test_integration.py b/testing/test_integration.py index 074785f9..c381998c 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -921,3 +921,35 @@ def test_integration_function_call_order( assert final_version == expected_final_version, ( f"Expected version '{expected_final_version}' but got '{final_version}'" ) + + +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" From 117ad86f293be0a81b50e0ee16a66cdc80e03296 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:36:44 +0000 Subject: [PATCH 090/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.5 → v0.12.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.5...v0.12.7) - [github.com/pre-commit/mirrors-mypy: v1.17.0 → v1.17.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.0...v1.17.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 61fe40d0..ae795145 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.5 + rev: v0.12.7 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.0 + rev: v1.17.1 hooks: - id: mypy args: [--strict] From 73491bdf26955924b3bff0ea192af4c4c38009d8 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 5 Aug 2025 10:19:01 +0200 Subject: [PATCH 091/162] add codespell pre-commit hook --- .cursor/rules/test-running.mdc | 2 +- .pre-commit-config.yaml | 6 ++++++ CHANGELOG.md | 13 +++++++------ testing/test_version.py | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.cursor/rules/test-running.mdc b/.cursor/rules/test-running.mdc index 0bd595a2..201f9af6 100644 --- a/.cursor/rules/test-running.mdc +++ b/.cursor/rules/test-running.mdc @@ -1,5 +1,5 @@ --- -description: run tests wit uv tooling +description: run tests with uv tooling globs: alwaysApply: true --- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae795145..7c3dfd0b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,3 +31,9 @@ repos: rev: 2025.05.02 hooks: - id: sp-repo-review + +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + args: [-w, --ignore-words-list=hist,nd,te] diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1f30c6..9ec68c2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,15 @@ ### Fixed - fix #1180: ensure version dumping works when no scm_version is given (problems in downstreams) -- fix #1181: config - reintroduce controll over when we expect a section to be present - as it turns out theres valid use cases where setuptools_scm is not direct part of the dependencies +- fix #1181: config - reintroduce control over when we expect a section to be present + as it turns out there's valid use cases where setuptools_scm is not direct part of the dependencies +- add codespell pre-commit hook ## v9.0.0 ### Breaking -- fix #1019: pass python version build tags from scm version to results propperly +- fix #1019: pass python version build tags from scm version to results properly ### Added @@ -38,7 +39,7 @@ - fix #1150: enable setuptools-scm when we are a build requirement - feature #1154: add the commit id the the default version file template - drop scriv -- fix #921: document setuptools version requirements more consistently - 61 as minumum asn 8 as recommended minimum +- fix #921: document setuptools version requirements more consistently - 61 as minimum asn 8 as recommended minimum ### Fixed @@ -51,7 +52,7 @@ - fix #879: add test that validates case different behavior on windows - migrate git describe command to new scm config - add support for failing on missing submodules -- fix #279: expand errors when scm can be found upwards and relative_to wasnt used +- fix #279: expand errors when scm can be found upwards and relative_to wasn't used - fix #577: introduce explicit scmversion node and short node - fix #1100: add workaround for readthedocs worktress to the docs - fix #790: document shallow fail for rtd @@ -72,7 +73,7 @@ ### Fixed -- fixed #1131: allow self-build without importlib_metadata avaliable on python3.9 +- fixed #1131: allow self-build without importlib_metadata available on python3.9 ## v8.3.0 diff --git a/testing/test_version.py b/testing/test_version.py index 71850074..e63c9493 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -514,13 +514,13 @@ def local(self) -> str | None: def test_no_matching_entrypoints(config_key: str) -> None: version = meta( "1.0", - config=replace(c, **{config_key: "nonexistant"}), # type: ignore[arg-type] + config=replace(c, **{config_key: "nonexistent"}), # type: ignore[arg-type] ) with pytest.raises( ValueError, match=( r'Couldn\'t find any implementations for entrypoint "setuptools_scm\..*?"' - ' with value "nonexistant"' + ' with value "nonexistent"' ), ): format_version(version) From 0f7c71d6d90b7ccfc9157955aeff3bdebcebe798 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 5 Aug 2025 18:32:23 +0200 Subject: [PATCH 092/162] in case infer-version finds neither the config section, nor the requirement - skip action closes #1185 --- CHANGELOG.md | 6 + src/setuptools_scm/_config.py | 9 +- .../_integration/pyproject_reading.py | 7 +- src/setuptools_scm/_integration/setuptools.py | 16 ++- testing/test_integration.py | 119 ++++++++++++++++++ 5 files changed, 152 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ec68c2d..4b850cf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v9.0.2 + +### Fixed + +- fix #1184: in case setuptools-scm is a indirect dependency and no pyproject.toml section exists - don't infer the version + ## v9.0.1 diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index c4ebc773..fbc53636 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -272,6 +272,7 @@ def from_file( dist_name: str | None = None, missing_file_ok: bool = False, missing_section_ok: bool = False, + pyproject_data: PyProjectData | None = None, **kwargs: Any, ) -> Configuration: """ @@ -291,9 +292,10 @@ def from_file( """ try: - pyproject_data = _read_pyproject( - Path(name), missing_section_ok=missing_section_ok - ) + if pyproject_data is None: + pyproject_data = _read_pyproject( + Path(name), missing_section_ok=missing_section_ok + ) except FileNotFoundError: if missing_file_ok: log.warning("File %s not found, using empty configuration", name) @@ -303,6 +305,7 @@ def from_file( project={}, section={}, is_required=False, + section_present=False, ) else: raise diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 8e59ce4e..7c3108a3 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -22,6 +22,7 @@ class PyProjectData(NamedTuple): project: TOML_RESULT section: TOML_RESULT is_required: bool + section_present: bool @property def project_name(self) -> str | None: @@ -55,6 +56,7 @@ def read_pyproject( 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 @@ -69,9 +71,12 @@ def read_pyproject( 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 project = defn.get("project", {}) - return PyProjectData(path, tool_name, project, section, is_required) + return PyProjectData( + path, tool_name, project, section, is_required, section_present + ) def get_args_for_pyproject( diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index e5ff73e5..7f76dfd3 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -4,6 +4,7 @@ import os import warnings +from pathlib import Path from typing import Any from typing import Callable @@ -149,8 +150,21 @@ def infer_version(dist: setuptools.Distribution) -> None: if dist_name == "setuptools-scm": return + # Check if setuptools-scm is configured before proceeding try: - config = _config.Configuration.from_file(dist_name=dist_name) + from .pyproject_reading import read_pyproject + + pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True) + # Only proceed if setuptools-scm is either in build_requires or has a tool section + if not pyproject_data.is_required and not pyproject_data.section_present: + return # No setuptools-scm configuration, silently return + except (FileNotFoundError, LookupError): + return # No pyproject.toml or other issues, silently return + + try: + config = _config.Configuration.from_file( + dist_name=dist_name, pyproject_data=pyproject_data + ) except LookupError as e: log.info(e, exc_info=True) else: diff --git a/testing/test_integration.py b/testing/test_integration.py index c381998c..e9383a8c 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -923,6 +923,125 @@ 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) + + import setuptools + + from setuptools_scm._integration.setuptools import infer_version + + # Create distribution + dist = setuptools.Distribution({"name": "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) + + import setuptools + + from setuptools_scm._integration.setuptools import infer_version + + # Create distribution + dist = setuptools.Distribution({"name": "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) + + import setuptools + + from setuptools_scm._integration.setuptools import infer_version + + # Create distribution + dist = setuptools.Distribution({"name": "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: From 749cc653a43024bb502929822253f4112326604a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 5 Aug 2025 21:09:04 +0200 Subject: [PATCH 093/162] verify dynamic version is requested when we use build-requirement as act indication closes #1184 --- CHANGELOG.md | 6 + .../_integration/pyproject_reading.py | 20 ++- src/setuptools_scm/_integration/setuptools.py | 4 + testing/test_integration.py | 135 +++++++++++++++++- 4 files changed, 163 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b850cf7..d77e15ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v9.0.3 + +### fixed + +- fix 1184: verify version is dynamic if the dependency is used as indicator for enabling + ## v9.0.2 ### Fixed diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 7c3108a3..d53778c2 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -28,6 +28,19 @@ class PyProjectData(NamedTuple): 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 + 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." + ) + def has_build_package( requires: Sequence[str], build_package_names: Sequence[str] @@ -74,10 +87,15 @@ def read_pyproject( section_present = False project = defn.get("project", {}) - return PyProjectData( + pyproject_data = PyProjectData( path, tool_name, project, section, is_required, section_present ) + # Verify dynamic version when setuptools-scm is used as build dependency indicator + pyproject_data.verify_dynamic_version_when_required() + + return pyproject_data + def get_args_for_pyproject( pyproject: PyProjectData, diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 7f76dfd3..565b5f42 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -160,6 +160,10 @@ def infer_version(dist: setuptools.Distribution) -> None: return # No setuptools-scm configuration, silently return except (FileNotFoundError, LookupError): return # No pyproject.toml or other issues, silently return + except ValueError as e: + # Log the error as debug info instead of raising it + log.debug("Configuration issue in pyproject.toml: %s", e) + return # Configuration issue, silently return try: config = _config.Configuration.from_file( diff --git a/testing/test_integration.py b/testing/test_integration.py index e9383a8c..0f100c1e 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -132,7 +132,7 @@ def test_pyproject_missing_setup_hook_works(wd: WorkDir, use_scm_version: str) - [build-system] requires=["setuptools", "setuptools_scm"] build-backend = "setuptools.build_meta" - [tool] + [tool.setuptools_scm] """ ), ) @@ -743,6 +743,7 @@ def test_build_requires_integration_with_config_reading(wd: WorkDir) -> None: [project] name = "test-package" + dynamic = ["version"] """ ), ) @@ -763,6 +764,7 @@ def test_build_requires_integration_with_config_reading(wd: WorkDir) -> None: [project] name = "test-package" + dynamic = ["version"] """ ), ) @@ -1072,3 +1074,134 @@ def test_version_keyword_no_scm_dependency_works( 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) + + import setuptools + + from setuptools_scm._integration.setuptools import infer_version + + # Create distribution + dist = setuptools.Distribution({"name": "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 From 8c0f1214ed914841c7b40fe97e0a5f6fde5df67d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 7 Aug 2025 13:14:29 +0200 Subject: [PATCH 094/162] refactor step for version inference - intorduce intermediate types and fix/simplify the tests --- src/setuptools_scm/_config.py | 24 +- .../_integration/pyproject_reading.py | 59 ++++- src/setuptools_scm/_integration/setup_cfg.py | 21 ++ src/setuptools_scm/_integration/setuptools.py | 153 ++++++------ .../_integration/version_inference.py | 118 ++++++++++ testing/test_basic_api.py | 2 +- testing/test_integration.py | 119 +++++++++- testing/test_pyproject_reading.py | 57 +++++ testing/test_version_inference.py | 219 ++++++++++++++++++ 9 files changed, 671 insertions(+), 101 deletions(-) create mode 100644 src/setuptools_scm/_integration/setup_cfg.py create mode 100644 src/setuptools_scm/_integration/version_inference.py create mode 100644 testing/test_pyproject_reading.py create mode 100644 testing/test_version_inference.py diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index fbc53636..7c1d185a 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -291,24 +291,12 @@ def from_file( - **kwargs: additional keyword arguments to pass to the Configuration constructor """ - try: - if pyproject_data is None: - pyproject_data = _read_pyproject( - Path(name), missing_section_ok=missing_section_ok - ) - 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, - section_present=False, - ) - else: - raise + if pyproject_data is None: + pyproject_data = _read_pyproject( + Path(name), + missing_section_ok=missing_section_ok, + missing_file_ok=missing_file_ok, + ) 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 d53778c2..55fa52f7 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -2,12 +2,11 @@ import warnings +from dataclasses import dataclass from pathlib import Path -from typing import NamedTuple from typing import Sequence from .. import _log -from .setuptools import read_dist_name_from_setup_cfg from .toml import TOML_RESULT from .toml import read_toml_content @@ -16,13 +15,39 @@ _ROOT = "root" -class PyProjectData(NamedTuple): +@dataclass +class PyProjectData: path: Path tool_name: str project: TOML_RESULT section: TOML_RESULT is_required: bool section_present: bool + project_present: bool + + @classmethod + def for_testing( + cls, + is_required: bool = False, + section_present: bool = False, + project_present: bool = False, + project_name: str | None = None, + ) -> PyProjectData: + """Create a PyProjectData instance for testing purposes.""" + if project_name is not None: + project = {"name": project_name} + assert project_present + else: + project = {} + return cls( + path=Path("pyproject.toml"), + tool_name="setuptools_scm", + project=project, + section={}, + is_required=is_required, + section_present=section_present, + project_present=project_present, + ) @property def project_name(self) -> str | None: @@ -33,6 +58,10 @@ def verify_dynamic_version_when_required(self) -> None: 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( @@ -62,8 +91,25 @@ def read_pyproject( tool_name: str = "setuptools_scm", build_package_names: Sequence[str] = ("setuptools_scm", "setuptools-scm"), missing_section_ok: bool = False, + missing_file_ok: bool = False, ) -> PyProjectData: - defn = read_toml_content(path) + 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 + requires: list[str] = defn.get("build-system", {}).get("requires", []) is_required = has_build_package(requires, build_package_names) @@ -87,8 +133,9 @@ def read_pyproject( section_present = False project = defn.get("project", {}) + project_present = "project" in defn pyproject_data = PyProjectData( - path, tool_name, project, section, is_required, section_present + path, tool_name, project, section, is_required, section_present, project_present ) # Verify dynamic version when setuptools-scm is used as build dependency indicator @@ -121,8 +168,6 @@ def get_args_for_pyproject( if dist_name is None: # minimal pep 621 support for figuring the pretend keys dist_name = pyproject.project_name - if dist_name is None: - dist_name = read_dist_name_from_setup_cfg() if _ROOT in kwargs: if kwargs[_ROOT] is None: kwargs.pop(_ROOT, None) diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/src/setuptools_scm/_integration/setup_cfg.py new file mode 100644 index 00000000..e904d7d1 --- /dev/null +++ b/src/setuptools_scm/_integration/setup_cfg.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import os + +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 + import configparser + + parser = configparser.ConfigParser() + parser.read([input], encoding="utf-8") + dist_name = parser.get("metadata", "name", fallback=None) + return dist_name + + +def _dist_name_from_legacy(dist: setuptools.Distribution) -> str | None: + return dist.metadata.name or read_dist_name_from_setup_cfg() diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 565b5f42..fefe4be6 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -import os import warnings from pathlib import Path @@ -11,22 +10,16 @@ import setuptools from .. import _config +from .pyproject_reading import read_pyproject +from .setup_cfg import _dist_name_from_legacy +from .version_inference import VersionInferenceConfig +from .version_inference import VersionInferenceError +from .version_inference import VersionInferenceException +from .version_inference import get_version_inference_config log = logging.getLogger(__name__) -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 - import configparser - - parser = configparser.ConfigParser() - parser.read([input], encoding="utf-8") - dist_name = parser.get("metadata", "name", fallback=None) - return dist_name - - def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: if int(_version.split(".")[0]) < 61: warnings.warn( @@ -85,7 +78,7 @@ def _assign_version( def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None: - log.debug("%s %r", hook, vars(dist.metadata)) + log.debug("%s %s %s %r", hook, id(dist), id(dist.metadata), vars(dist.metadata)) def version_keyword( @@ -93,6 +86,7 @@ def version_keyword( keyword: str, value: bool | dict[str, Any] | Callable[[], dict[str, Any]], ) -> None: + # Parse overrides (integration point responsibility) overrides: dict[str, Any] if value is True: overrides = {} @@ -105,73 +99,96 @@ def version_keyword( assert "dist_name" not in overrides, ( "dist_name may not be specified in the setup keyword " ) - dist_name: str | None = dist.metadata.name + + dist_name: str | None = _dist_name_from_legacy(dist) + + was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) _log_hookstart("version_keyword", dist) - if dist.metadata.version is not None: - # Check if version was set by infer_version - was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) + # Get pyproject data + try: + pyproject_data = read_pyproject( + Path("pyproject.toml"), missing_section_ok=True, missing_file_ok=True + ) + except (LookupError, ValueError) as e: + log.debug("Configuration issue in pyproject.toml: %s", e) + return + # Get decision + result = get_version_inference_config( + dist_name=dist_name, + current_version=dist.metadata.version, + pyproject_data=pyproject_data, + overrides=overrides, + was_set_by_infer=was_set_by_infer, + ) + + # Handle result + if result is None: + return # Don't infer + elif isinstance(result, VersionInferenceError): + if result.should_warn: + warnings.warn(result.message) + return + elif isinstance(result, VersionInferenceException): + raise result.exception + elif isinstance(result, VersionInferenceConfig): + # Clear version if it was set by infer_version if was_set_by_infer: - # Version was set by infer_version, check if we have overrides - if not overrides: - # No overrides, just use the infer_version result - return - # We have overrides, clear the marker and proceed to override the version dist._setuptools_scm_version_set_by_infer = False # type: ignore[attr-defined] dist.metadata.version = None - else: - # Version was set by something else, warn and return - warnings.warn(f"version of {dist_name} already set") - return - - if dist_name is None: - dist_name = read_dist_name_from_setup_cfg() - config = _config.Configuration.from_file( - dist_name=dist_name, - missing_file_ok=True, - missing_section_ok=True, - **overrides, - ) - _assign_version(dist, config) + # Proceed with inference + config = _config.Configuration.from_file( + dist_name=result.dist_name, + pyproject_data=result.pyproject_data, + missing_file_ok=True, + missing_section_ok=True, + **overrides, + ) + _assign_version(dist, config) def infer_version(dist: setuptools.Distribution) -> None: _log_hookstart("infer_version", dist) - log.debug("dist %s %s", id(dist), id(dist.metadata)) - if dist.metadata.version is not None: - return # metadata already added by hook - dist_name = dist.metadata.name - if dist_name is None: - dist_name = read_dist_name_from_setup_cfg() - if not os.path.isfile("pyproject.toml"): - return - if dist_name == "setuptools-scm": - return - # Check if setuptools-scm is configured before proceeding - try: - from .pyproject_reading import read_pyproject + dist_name = _dist_name_from_legacy(dist) + # Get pyproject data (integration point responsibility) + try: pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True) - # Only proceed if setuptools-scm is either in build_requires or has a tool section - if not pyproject_data.is_required and not pyproject_data.section_present: - return # No setuptools-scm configuration, silently return - except (FileNotFoundError, LookupError): - return # No pyproject.toml or other issues, silently return - except ValueError as e: - # Log the error as debug info instead of raising it + except FileNotFoundError: + log.debug("pyproject.toml not found") + return + except (LookupError, ValueError) as e: log.debug("Configuration issue in pyproject.toml: %s", e) - return # Configuration issue, silently return + return - try: - config = _config.Configuration.from_file( - dist_name=dist_name, pyproject_data=pyproject_data - ) - except LookupError as e: - log.info(e, exc_info=True) - else: - _assign_version(dist, config) - # Mark that this version was set by infer_version - dist._setuptools_scm_version_set_by_infer = True # type: ignore[attr-defined] + # Get decision + result = get_version_inference_config( + dist_name=dist_name, + current_version=dist.metadata.version, + pyproject_data=pyproject_data, + ) + + # Handle result + if result is None: + return # Don't infer + elif isinstance(result, VersionInferenceError): + if result.should_warn: + log.warning(result.message) + return + elif isinstance(result, VersionInferenceException): + raise result.exception + elif isinstance(result, VersionInferenceConfig): + # Proceed with inference + try: + config = _config.Configuration.from_file( + dist_name=result.dist_name, pyproject_data=result.pyproject_data + ) + except LookupError as e: + log.info(e, exc_info=True) + else: + _assign_version(dist, config) + # Mark that this version was set by infer_version + dist._setuptools_scm_version_set_by_infer = True # type: ignore[attr-defined] diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py new file mode 100644 index 00000000..d787d214 --- /dev/null +++ b/src/setuptools_scm/_integration/version_inference.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING +from typing import Any +from typing import Union + +from .. import _log + +if TYPE_CHECKING: + from .pyproject_reading import PyProjectData + +log = _log.log.getChild("version_inference") + + +@dataclass +class VersionInferenceConfig: + """Configuration for version inference.""" + + dist_name: str | None + pyproject_data: PyProjectData | None + overrides: dict[str, Any] + + +@dataclass +class VersionInferenceError: + """Error message for user.""" + + message: str + should_warn: bool = False + + +@dataclass +class VersionInferenceException: + """Exception that should be raised.""" + + exception: Exception + + +VersionInferenceResult = Union[ + VersionInferenceConfig, # Proceed with inference + VersionInferenceError, # Show error/warning + VersionInferenceException, # Raise exception + None, # Don't infer (silent) +] + + +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. + + Args: + dist_name: The distribution name + 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: + # Clear version and proceed with overrides + return VersionInferenceConfig( + dist_name=dist_name, + pyproject_data=pyproject_data, + overrides=overrides, + ) + else: + # Keep existing version from infer_version + return None + 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 None + + # Handle missing configuration + if not pyproject_data.is_required and not pyproject_data.section_present: + # If there are overrides, proceed with inference (explicit use_scm_version) + if overrides is not None: + return VersionInferenceConfig( + dist_name=dist_name, + pyproject_data=pyproject_data, + overrides=overrides, + ) + return None + + # Handle missing project section when required + if ( + pyproject_data.is_required + and not pyproject_data.section_present + and not pyproject_data.project_present + ): + return None + + # All conditions met - proceed with inference + return VersionInferenceConfig( + dist_name=dist_name, + pyproject_data=pyproject_data, + overrides=overrides or {}, + ) diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py index ca0c3041..7847b352 100644 --- a/testing/test_basic_api.py +++ b/testing/test_basic_api.py @@ -106,7 +106,7 @@ def test_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_empty_pretend_version(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") + # monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION", "") p = tmp_path / "sub/package" p.mkdir(parents=True) diff --git a/testing/test_integration.py b/testing/test_integration.py index 0f100c1e..a85b6ca3 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -71,10 +71,10 @@ def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> N PYPROJECT_FILES = { - "setup.py": "[tool.setuptools_scm]", - "setup.cfg": "[tool.setuptools_scm]", + "setup.py": "[tool.setuptools_scm]\n", + "setup.cfg": "[tool.setuptools_scm]\n", "pyproject tool.setuptools_scm": ( - "[tool.setuptools_scm]\ndist_name='setuptools_scm_example'" + "[project]\nname='setuptools_scm_example'\n[tool.setuptools_scm]" ), "pyproject.project": ( "[project]\nname='setuptools_scm_example'\n" @@ -109,11 +109,116 @@ def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> N def test_pyproject_support_with_git(wd: WorkDir, metadata_in: str) -> None: if sys.version_info < (3, 11): pytest.importorskip("tomli") - wd.write("pyproject.toml", PYPROJECT_FILES[metadata_in]) + + # 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]) - res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("0.1.dev0+d20090213") + + # 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: {}"]) @@ -539,7 +644,7 @@ def test_unicode_in_setup_cfg(tmp_path: Path) -> None: ), encoding="utf-8", ) - name = setuptools_scm._integration.setuptools.read_dist_name_from_setup_cfg(cfg) + name = setuptools_scm._integration.setup_cfg.read_dist_name_from_setup_cfg(cfg) assert name == "configparser" diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py new file mode 100644 index 00000000..ea7c1a3a --- /dev/null +++ b/testing/test_pyproject_reading.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +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.""" + with pytest.raises(FileNotFoundError): + read_pyproject(path=tmp_path / "nonexistent.toml", missing_file_ok=False) + + def test_read_pyproject_existing_file(self, tmp_path: Path) -> None: + """Test that read_pyproject reads existing files correctly.""" + # Create a simple pyproject.toml + pyproject_content = """ +[build-system] +requires = ["setuptools>=80", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "test-package" +dynamic = ["version"] + +[tool.setuptools_scm] +""" + pyproject_file = tmp_path / "pyproject.toml" + pyproject_file.write_text(pyproject_content) + + result = read_pyproject(path=pyproject_file) + + assert result.path == pyproject_file + assert result.tool_name == "setuptools_scm" + assert result.is_required is True + assert result.section_present is True + assert result.project_present is True + assert result.project.get("name") == "test-package" diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py new file mode 100644 index 00000000..6fc10539 --- /dev/null +++ b/testing/test_version_inference.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +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 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 don't proceed when version was set by infer_version with no overrides.""" + 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 result is None + + 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 result is None + + 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(self) -> None: + """Test that we don't infer when setuptools-scm is not configured.""" + result = get_version_inference_config( + dist_name="test_package", + current_version=None, + pyproject_data=PyProjectData.for_testing(False, False, True), + ) + + assert result is None + + def test_setuptools_scm_required_no_project_section(self) -> None: + """Test that we don't infer when setuptools-scm is required but no project section.""" + result = get_version_inference_config( + dist_name="test_package", + current_version=None, + pyproject_data=PyProjectData.for_testing(True, False, False), + ) + + assert result is None + + 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", + current_version=None, + pyproject_data=PyProjectData.for_testing(True, False, True), + ) + + 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", + current_version=None, + pyproject_data=PyProjectData.for_testing(False, True, False), + ) + + 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", + current_version=None, + pyproject_data=PyProjectData.for_testing(True, True, True), + ) + + 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, + current_version=None, + pyproject_data=PyProjectData.for_testing(True, True, True), + ) + + 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, + ) + + 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", + current_version=None, + pyproject_data=PyProjectData.for_testing(True, True, True), + overrides=overrides, + ) + + 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", + pyproject_data=pyproject_data, + overrides={"key": "value"}, + ) + + 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 From d379ba208a95eaafb5aef6b37377c6d7929896a6 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 7 Aug 2025 13:21:29 +0200 Subject: [PATCH 095/162] simplify pyproject --- pyproject.toml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a8345310..78b8f437 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,11 +48,8 @@ dependencies = [ 'typing-extensions; python_version < "3.10"', ] [project.optional-dependencies] -rich = [ - "rich", -] -toml = [ -] +rich = ["rich"] +toml = [] [dependency-groups] docs = [ From c37f631f7afcf2d2fbb4630d03e59ec23e95fc99 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 7 Aug 2025 13:21:59 +0200 Subject: [PATCH 096/162] use parametrize in test_extract_package_name --- testing/test_integration.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/testing/test_integration.py b/testing/test_integration.py index a85b6ca3..8c3b5822 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -818,17 +818,15 @@ def test_pyproject_build_system_requires_priority_over_tool_section( assert res.endswith("0.1.dev0+d20090213") -def test_extract_package_name() -> None: +@pytest.mark.parametrize("base_name", ["setuptools_scm", "setuptools-scm"]) +@pytest.mark.parametrize( + "requirements", + ["", ">=8", "[toml]>=7", "~=9.0", "[rich,toml]>=8"], + ids=["empty", "version", "extras", "fuzzy", "multiple-extras"], +) +def test_extract_package_name(base_name: str, requirements: str) -> 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" + assert _extract_package_name(f"{base_name}{requirements}") == base_name def test_build_requires_integration_with_config_reading(wd: WorkDir) -> None: From abb61854fce7c6474ece95190b6b4101d123faa2 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 7 Aug 2025 22:30:51 +0200 Subject: [PATCH 097/162] use the canonical parsers from packaging for getting the package name --- .../_integration/pyproject_reading.py | 8 ++--- src/setuptools_scm/_integration/setuptools.py | 19 ----------- src/setuptools_scm/_requirement_cls.py | 32 +++++++++++++++++++ testing/test_integration.py | 5 +-- 4 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 src/setuptools_scm/_requirement_cls.py diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 55fa52f7..a98f5643 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -7,6 +7,7 @@ from typing import Sequence from .. import _log +from .._requirement_cls import extract_package_name from .toml import TOML_RESULT from .toml import read_toml_content @@ -75,12 +76,7 @@ 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() + package_name = extract_package_name(requirement).lower() if package_name in build_package_names: return True return False diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index fefe4be6..d61b7516 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -39,25 +39,6 @@ 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: diff --git a/src/setuptools_scm/_requirement_cls.py b/src/setuptools_scm/_requirement_cls.py new file mode 100644 index 00000000..810e91fa --- /dev/null +++ b/src/setuptools_scm/_requirement_cls.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +try: + from packaging.requirements import Requirement + from packaging.utils import canonicalize_name +except ImportError: + from setuptools.extern.packaging.requirements import ( # type: ignore[import-not-found,no-redef] + Requirement as Requirement, + ) + from setuptools.extern.packaging.utils import ( # type: ignore[import-not-found,no-redef] + canonicalize_name as canonicalize_name, + ) + +from . import _log + +log = _log.log.getChild("requirement_cls") + + +def extract_package_name(requirement_string: str) -> str: + """Extract the canonical package name from a requirement string. + + This function uses packaging.requirements.Requirement to properly parse + the requirement and extract the package name, handling all edge cases + that the custom regex-based approach might miss. + + Args: + requirement_string: The requirement string to parse + + Returns: + The package name as a string + """ + return canonicalize_name(Requirement(requirement_string).name) diff --git a/testing/test_integration.py b/testing/test_integration.py index 8c3b5822..0b25149b 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -18,11 +18,12 @@ import setuptools_scm._integration.setuptools +from setuptools_scm._requirement_cls import extract_package_name + if TYPE_CHECKING: import 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 @@ -826,7 +827,7 @@ def test_pyproject_build_system_requires_priority_over_tool_section( ) def test_extract_package_name(base_name: str, requirements: str) -> None: """Test the _extract_package_name helper function""" - assert _extract_package_name(f"{base_name}{requirements}") == base_name + assert extract_package_name(f"{base_name}{requirements}") == "setuptools-scm" def test_build_requires_integration_with_config_reading(wd: WorkDir) -> None: From b76ba89b1e341103c92905cd3b76974c9cc5eb7e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 7 Aug 2025 23:23:59 +0200 Subject: [PATCH 098/162] finish migrating to using canonical package nmes for match setuptools_scm in bild deps --- src/setuptools_scm/_integration/pyproject_reading.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index a98f5643..df5d30c8 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -73,11 +73,11 @@ def verify_dynamic_version_when_required(self) -> None: def has_build_package( - requires: Sequence[str], build_package_names: Sequence[str] + requires: Sequence[str], canonical_build_package_name: str ) -> bool: for requirement in requires: - package_name = extract_package_name(requirement).lower() - if package_name in build_package_names: + package_name = extract_package_name(requirement) + if package_name == canonical_build_package_name: return True return False @@ -85,7 +85,7 @@ def has_build_package( def read_pyproject( path: Path = Path("pyproject.toml"), tool_name: str = "setuptools_scm", - build_package_names: Sequence[str] = ("setuptools_scm", "setuptools-scm"), + canonical_build_package_name: str = "setuptools-scm", missing_section_ok: bool = False, missing_file_ok: bool = False, ) -> PyProjectData: @@ -107,7 +107,7 @@ def read_pyproject( raise requires: list[str] = defn.get("build-system", {}).get("requires", []) - is_required = has_build_package(requires, build_package_names) + is_required = has_build_package(requires, canonical_build_package_name) try: section = defn.get("tool", {})[tool_name] From e8e5c459e5f811211043c113fbc7ac58bcfdba15 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 10 Aug 2025 08:49:21 +0200 Subject: [PATCH 099/162] correct encoding and manifest oversights --- MANIFEST.in | 2 ++ testing/test_better_root_errors.py | 2 +- testing/test_git.py | 4 ++-- testing/test_integration.py | 4 ++-- testing/test_pyproject_reading.py | 2 +- testing/test_regressions.py | 9 ++++++--- tox.ini | 7 ++++++- 7 files changed, 20 insertions(+), 10 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 6b9e3204..b793e6c0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -18,8 +18,10 @@ include CHANGELOG.md recursive-include testing *.bash prune nextgen +prune .cursor recursive-include docs *.md include docs/examples/version_scheme_code/*.py include docs/examples/version_scheme_code/*.toml include mkdocs.yml +include uv.lock \ No newline at end of file diff --git a/testing/test_better_root_errors.py b/testing/test_better_root_errors.py index 31d7733d..0ba964cc 100644 --- a/testing/test_better_root_errors.py +++ b/testing/test_better_root_errors.py @@ -138,7 +138,7 @@ def test_version_missing_with_relative_to_set(wd: WorkDir) -> None: # Create a dummy file to use as relative_to dummy_file = subdir / "setup.py" - dummy_file.write_text("# dummy file") + dummy_file.write_text("# dummy file", encoding="utf-8") # Test error message when relative_to IS set config = Configuration(root=str(subdir), relative_to=str(dummy_file)) diff --git a/testing/test_git.py b/testing/test_git.py index ab105dd1..31cac7a3 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -666,7 +666,7 @@ def test_fail_on_missing_submodules_with_initialized_submodules(wd: WorkDir) -> # Create a commit in the submodule test_file = submodule_dir / "test.txt" - test_file.write_text("test content") + test_file.write_text("test content", encoding="utf-8") wd(["git", "-C", str(submodule_dir), "add", "test.txt"]) wd(["git", "-C", str(submodule_dir), "commit", "-m", "Initial commit"]) @@ -741,7 +741,7 @@ def test_nested_scm_git_config_from_toml(tmp_path: Path) -> None: [tool.setuptools_scm.scm.git] pre_parse = "fail_on_missing_submodules" """ - pyproject_path.write_text(pyproject_content) + pyproject_path.write_text(pyproject_content, encoding="utf-8") # Parse the configuration from file config = Configuration.from_file(pyproject_path) diff --git a/testing/test_integration.py b/testing/test_integration.py index 0b25149b..b2cbd885 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -339,7 +339,7 @@ def test_pretend_metadata_with_version( write_to="src/version.py", write_to_template=version_file_content ) - content = (wd.cwd / "src/version.py").read_text() + content = (wd.cwd / "src/version.py").read_text(encoding="utf-8") assert "commit_hash = 'g1337beef'" in content assert "num_commit = 4" in content @@ -412,7 +412,7 @@ def test_pretend_metadata_with_scm_version( write_to="src/version.py", write_to_template=version_file_content ) - content = (wd.cwd / "src/version.py").read_text() + content = (wd.cwd / "src/version.py").read_text(encoding="utf-8") assert "commit_hash = 'gcustom123'" in content assert "num_commit = 7" in content diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py index ea7c1a3a..592adf86 100644 --- a/testing/test_pyproject_reading.py +++ b/testing/test_pyproject_reading.py @@ -45,7 +45,7 @@ def test_read_pyproject_existing_file(self, tmp_path: Path) -> None: [tool.setuptools_scm] """ pyproject_file = tmp_path / "pyproject.toml" - pyproject_file.write_text(pyproject_content) + pyproject_file.write_text(pyproject_content, encoding="utf-8") result = read_pyproject(path=pyproject_file) diff --git a/testing/test_regressions.py b/testing/test_regressions.py index 679365e6..326d62b8 100644 --- a/testing/test_regressions.py +++ b/testing/test_regressions.py @@ -114,7 +114,8 @@ def test_case_mismatch_nested_dir_windows_git(tmp_path: Path) -> None: nested_dir.mkdir() # Create a pyproject.toml in the nested directory - (nested_dir / "pyproject.toml").write_text(""" + (nested_dir / "pyproject.toml").write_text( + """ [build-system] requires = ["setuptools>=64", "setuptools-scm"] build-backend = "setuptools.build_meta" @@ -124,7 +125,9 @@ def test_case_mismatch_nested_dir_windows_git(tmp_path: Path) -> None: dynamic = ["version"] [tool.setuptools_scm] -""") +""", + encoding="utf-8", + ) # Add and commit the file run("git add .", repo_path) @@ -159,7 +162,7 @@ def test_case_mismatch_force_assertion_failure(tmp_path: Path) -> None: nested_dir.mkdir() # Add and commit something to make it a valid repo - (nested_dir / "test.txt").write_text("test") + (nested_dir / "test.txt").write_text("test", encoding="utf-8") run("git add .", repo_path) run("git commit -m 'Initial commit'", repo_path) diff --git a/tox.ini b/tox.ini index cdb25590..83af3bbe 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,12 @@ ignore=E203,W503 [testenv] usedevelop=True -extras=test +dependency_groups = test +deps = + pytest + pytest-cov + pytest-timeout + pytest-xdist commands= python -X warn_default_encoding -m pytest {posargs} From 8e177967853f30ed7bf5e49ebb2ea394be7d5bcb Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 11 Aug 2025 10:25:40 +0200 Subject: [PATCH 100/162] split out normalizing version keywork normalization --- src/setuptools_scm/_integration/setuptools.py | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index d61b7516..17031d27 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -62,20 +62,28 @@ def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None: log.debug("%s %s %s %r", hook, id(dist), id(dist.metadata), vars(dist.metadata)) +def get_keyword_overrides( + value: bool | dict[str, Any] | Callable[[], dict[str, Any]], +) -> dict[str, Any]: + """normalize the version keyword input""" + if value is True: + return {} + elif callable(value): + return value() + else: + assert isinstance(value, dict), "version_keyword expects a dict or True" + return value + + def version_keyword( dist: setuptools.Distribution, keyword: str, value: bool | dict[str, Any] | Callable[[], dict[str, Any]], ) -> None: + _log_hookstart("version_keyword", dist) + # Parse overrides (integration point responsibility) - overrides: dict[str, Any] - if value is True: - overrides = {} - elif callable(value): - overrides = value() - else: - assert isinstance(value, dict), "version_keyword expects a dict or True" - overrides = value + overrides = get_keyword_overrides(value) assert "dist_name" not in overrides, ( "dist_name may not be specified in the setup keyword " @@ -84,7 +92,6 @@ def version_keyword( dist_name: str | None = _dist_name_from_legacy(dist) was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) - _log_hookstart("version_keyword", dist) # Get pyproject data try: @@ -139,7 +146,7 @@ def infer_version(dist: setuptools.Distribution) -> None: try: pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True) except FileNotFoundError: - log.debug("pyproject.toml not found") + log.debug("pyproject.toml not found, skipping infer_version") return except (LookupError, ValueError) as e: log.debug("Configuration issue in pyproject.toml: %s", e) From 402fc25bcd6aef36bef3e41c182d318e84619010 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 11 Aug 2025 11:26:53 +0200 Subject: [PATCH 101/162] turn the version inference config into a protocol and simplify the setuptools integration points - test fixes : make clean distribution objects --- src/setuptools_scm/_integration/setuptools.py | 89 ++++--------------- .../_integration/version_inference.py | 81 ++++++++++++++--- testing/test_integration.py | 49 ++++++---- testing/test_version_inference.py | 25 ++++-- 4 files changed, 137 insertions(+), 107 deletions(-) diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 17031d27..6a98656a 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -3,18 +3,13 @@ import logging import warnings -from pathlib import Path from typing import Any from typing import Callable import setuptools -from .. import _config from .pyproject_reading import read_pyproject from .setup_cfg import _dist_name_from_legacy -from .version_inference import VersionInferenceConfig -from .version_inference import VersionInferenceError -from .version_inference import VersionInferenceException from .version_inference import get_version_inference_config log = logging.getLogger(__name__) @@ -39,22 +34,6 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: ) -def _assign_version( - dist: setuptools.Distribution, config: _config.Configuration -) -> None: - from .._get_version_impl import _get_version - from .._get_version_impl import _version_missing - - # todo: build time plugin - 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 - - _warn_on_old_setuptools() @@ -80,6 +59,10 @@ def version_keyword( keyword: str, value: bool | dict[str, Any] | Callable[[], dict[str, Any]], ) -> None: + """apply version infernce when setup(use_scm_version=...) is used + this takes priority over the finalize_options based version + """ + _log_hookstart("version_keyword", dist) # Parse overrides (integration point responsibility) @@ -95,14 +78,11 @@ def version_keyword( # Get pyproject data try: - pyproject_data = read_pyproject( - Path("pyproject.toml"), missing_section_ok=True, missing_file_ok=True - ) + pyproject_data = read_pyproject(missing_section_ok=True, missing_file_ok=True) except (LookupError, ValueError) as e: log.debug("Configuration issue in pyproject.toml: %s", e) return - # Get decision result = get_version_inference_config( dist_name=dist_name, current_version=dist.metadata.version, @@ -111,40 +91,23 @@ def version_keyword( was_set_by_infer=was_set_by_infer, ) - # Handle result - if result is None: - return # Don't infer - elif isinstance(result, VersionInferenceError): - if result.should_warn: - warnings.warn(result.message) - return - elif isinstance(result, VersionInferenceException): - raise result.exception - elif isinstance(result, VersionInferenceConfig): - # Clear version if it was set by infer_version - if was_set_by_infer: - dist._setuptools_scm_version_set_by_infer = False # type: ignore[attr-defined] - dist.metadata.version = None - - # Proceed with inference - config = _config.Configuration.from_file( - dist_name=result.dist_name, - pyproject_data=result.pyproject_data, - missing_file_ok=True, - missing_section_ok=True, - **overrides, - ) - _assign_version(dist, config) + result.apply(dist) def infer_version(dist: setuptools.Distribution) -> 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 + + if the version keyword is used, it will override the version from this hook + as user might have passed custom code version schemes + """ + _log_hookstart("infer_version", dist) dist_name = _dist_name_from_legacy(dist) - # Get pyproject data (integration point responsibility) try: - pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True) + pyproject_data = read_pyproject(missing_section_ok=True) except FileNotFoundError: log.debug("pyproject.toml not found, skipping infer_version") return @@ -152,31 +115,9 @@ def infer_version(dist: setuptools.Distribution) -> None: log.debug("Configuration issue in pyproject.toml: %s", e) return - # Get decision result = get_version_inference_config( dist_name=dist_name, current_version=dist.metadata.version, pyproject_data=pyproject_data, ) - - # Handle result - if result is None: - return # Don't infer - elif isinstance(result, VersionInferenceError): - if result.should_warn: - log.warning(result.message) - return - elif isinstance(result, VersionInferenceException): - raise result.exception - elif isinstance(result, VersionInferenceConfig): - # Proceed with inference - try: - config = _config.Configuration.from_file( - dist_name=result.dist_name, pyproject_data=result.pyproject_data - ) - except LookupError as e: - log.info(e, exc_info=True) - else: - _assign_version(dist, config) - # Mark that this version was set by infer_version - dist._setuptools_scm_version_set_by_infer = True # type: ignore[attr-defined] + result.apply(dist) diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index d787d214..5fd4991e 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -19,7 +19,40 @@ class VersionInferenceConfig: dist_name: str | None pyproject_data: PyProjectData | None - overrides: dict[str, Any] + overrides: dict[str, Any] | None + + def apply(self, dist: Any) -> 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 {}), + ) + + # 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 + + # 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 @dataclass @@ -29,6 +62,13 @@ class VersionInferenceError: message: str should_warn: bool = False + def apply(self, dist: Any) -> None: + """Apply error handling to the distribution.""" + import warnings + + if self.should_warn: + warnings.warn(self.message) + @dataclass class VersionInferenceException: @@ -36,12 +76,23 @@ class VersionInferenceException: exception: Exception + def apply(self, dist: Any) -> None: + """Apply exception handling to the distribution.""" + raise self.exception + + +class VersionInferenceNoOp: + """No operation result - silent skip.""" + + def apply(self, dist: Any) -> None: + """Apply no-op to the distribution.""" + VersionInferenceResult = Union[ VersionInferenceConfig, # Proceed with inference VersionInferenceError, # Show error/warning VersionInferenceException, # Raise exception - None, # Don't infer (silent) + VersionInferenceNoOp, # Don't infer (silent) ] @@ -71,16 +122,26 @@ def get_version_inference_config( # Handle version already set if current_version is not None: if was_set_by_infer: - if overrides is not None: - # Clear version and proceed with overrides + 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 - return None + # 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( @@ -89,7 +150,7 @@ def get_version_inference_config( # Handle setuptools-scm package if dist_name == "setuptools-scm": - return None + return VersionInferenceNoOp() # Handle missing configuration if not pyproject_data.is_required and not pyproject_data.section_present: @@ -100,7 +161,7 @@ def get_version_inference_config( pyproject_data=pyproject_data, overrides=overrides, ) - return None + return VersionInferenceNoOp() # Handle missing project section when required if ( @@ -108,11 +169,11 @@ def get_version_inference_config( and not pyproject_data.section_present and not pyproject_data.project_present ): - return None + return VersionInferenceNoOp() # All conditions met - proceed with inference return VersionInferenceConfig( dist_name=dist_name, pyproject_data=pyproject_data, - overrides=overrides or {}, + overrides=overrides, ) diff --git a/testing/test_integration.py b/testing/test_integration.py index b2cbd885..07527933 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -656,12 +656,12 @@ def test_setuptools_version_keyword_ensures_regex( wd.commit_testfile("test") wd("git tag 1.0") monkeypatch.chdir(wd.cwd) - import setuptools from setuptools_scm._integration.setuptools import version_keyword - dist = setuptools.Distribution({"name": "test"}) + dist = create_clean_distribution("test") version_keyword(dist, "use_scm_version", {"tag_regex": "(1.0)"}) + assert dist.metadata.version == "1.0" @pytest.mark.parametrize( @@ -917,6 +917,27 @@ def test_improved_error_message_mentions_both_config_options( 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. + + This function creates a new setuptools Distribution and ensures it's completely + clean from any previous setuptools_scm version inference effects, including: + - Clearing any existing version + - Removing the _setuptools_scm_version_set_by_infer flag + """ + import setuptools + + dist = setuptools.Distribution({"name": name}) + + # Clean all setuptools_scm effects + dist.metadata.version = None + if hasattr(dist, "_setuptools_scm_version_set_by_infer"): + delattr(dist, "_setuptools_scm_version_set_by_infer") + + return dist + + # Helper functions for testing integration point ordering def integration_infer_version(dist: setuptools.Distribution) -> str: """Helper to call infer_version and return the result.""" @@ -1053,12 +1074,10 @@ def test_infer_version_with_build_requires_no_tool_section( """ wd.write("pyproject.toml", pyproject_content) - import setuptools - from setuptools_scm._integration.setuptools import infer_version - # Create distribution - dist = setuptools.Distribution({"name": "test-package-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) @@ -1095,12 +1114,10 @@ def test_infer_version_with_build_requires_dash_variant_no_tool_section( """ wd.write("pyproject.toml", pyproject_content) - import setuptools - from setuptools_scm._integration.setuptools import infer_version - # Create distribution - dist = setuptools.Distribution({"name": "test-package-infer-version-dash"}) + # 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) @@ -1137,12 +1154,10 @@ def test_infer_version_without_build_requires_no_tool_section_silently_returns( """ wd.write("pyproject.toml", pyproject_content) - import setuptools - from setuptools_scm._integration.setuptools import infer_version - # Create distribution - dist = setuptools.Distribution({"name": "test-package-no-scm"}) + # Create clean distribution + dist = create_clean_distribution("test-package-no-scm") infer_version(dist) assert dist.metadata.version is None @@ -1297,12 +1312,10 @@ def test_infer_version_logs_debug_when_missing_dynamic_version( """ wd.write("pyproject.toml", pyproject_content) - import setuptools - from setuptools_scm._integration.setuptools import infer_version - # Create distribution - dist = setuptools.Distribution({"name": "test-package-missing-dynamic"}) + # 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) diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index 6fc10539..d6911358 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -4,6 +4,7 @@ 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 get_version_inference_config @@ -25,7 +26,7 @@ def test_version_already_set_by_infer_with_overrides(self) -> None: assert result.overrides == {"key": "value"} def test_version_already_set_by_infer_no_overrides(self) -> None: - """Test that we don't proceed when version was set by infer_version with no overrides.""" + """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", @@ -34,7 +35,21 @@ def test_version_already_set_by_infer_no_overrides(self) -> None: was_set_by_infer=True, ) - assert result is None + 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.""" @@ -58,7 +73,7 @@ def test_setuptools_scm_package(self) -> None: pyproject_data=PyProjectData.for_testing(True, True, True), ) - assert result is None + assert isinstance(result, VersionInferenceNoOp) def test_no_pyproject_toml(self) -> None: """Test that we don't infer when no pyproject.toml exists.""" @@ -74,7 +89,7 @@ def test_no_setuptools_scm_config(self) -> None: pyproject_data=PyProjectData.for_testing(False, False, True), ) - assert result is None + assert isinstance(result, VersionInferenceNoOp) def test_setuptools_scm_required_no_project_section(self) -> None: """Test that we don't infer when setuptools-scm is required but no project section.""" @@ -84,7 +99,7 @@ def test_setuptools_scm_required_no_project_section(self) -> None: pyproject_data=PyProjectData.for_testing(True, False, False), ) - assert result is None + assert isinstance(result, VersionInferenceNoOp) def test_setuptools_scm_required_with_project_section(self) -> None: """Test that we infer when setuptools-scm is required and project section exists.""" From f3c9a301467d046cac3917ee1cb0a910dcca7ca6 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 11 Aug 2025 11:35:21 +0200 Subject: [PATCH 102/162] simplify integration invocations in the integration tests --- testing/test_integration.py | 61 +++++++++++++------------------------ 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/testing/test_integration.py b/testing/test_integration.py index 07527933..89dc9cda 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -16,8 +16,7 @@ from packaging.version import Version -import setuptools_scm._integration.setuptools - +from setuptools_scm._integration import setuptools as setuptools_integration from setuptools_scm._requirement_cls import extract_package_name if TYPE_CHECKING: @@ -645,7 +644,9 @@ def test_unicode_in_setup_cfg(tmp_path: Path) -> None: ), encoding="utf-8", ) - name = setuptools_scm._integration.setup_cfg.read_dist_name_from_setup_cfg(cfg) + from setuptools_scm._integration.setup_cfg import read_dist_name_from_setup_cfg + + name = read_dist_name_from_setup_cfg(cfg) assert name == "configparser" @@ -657,10 +658,10 @@ def test_setuptools_version_keyword_ensures_regex( wd("git tag 1.0") monkeypatch.chdir(wd.cwd) - from setuptools_scm._integration.setuptools import version_keyword - dist = create_clean_distribution("test") - version_keyword(dist, "use_scm_version", {"tag_regex": "(1.0)"}) + setuptools_integration.version_keyword( + dist, "use_scm_version", {"tag_regex": "(1.0)"} + ) assert dist.metadata.version == "1.0" @@ -938,43 +939,32 @@ def create_clean_distribution(name: str) -> setuptools.Distribution: return dist -# Helper functions for testing integration point ordering -def integration_infer_version(dist: setuptools.Distribution) -> str: - """Helper to call infer_version and return the result.""" - from setuptools_scm._integration.setuptools import infer_version - - infer_version(dist) - return "infer_version" - - -def integration_version_keyword_default(dist: setuptools.Distribution) -> str: +def version_keyword_default(dist: setuptools.Distribution) -> None: """Helper to call version_keyword with default config and return the result.""" - from setuptools_scm._integration.setuptools import version_keyword - version_keyword(dist, "use_scm_version", True) - return "version_keyword_default" + setuptools_integration.version_keyword(dist, "use_scm_version", True) -def integration_version_keyword_calver(dist: setuptools.Distribution) -> str: +def version_keyword_calver(dist: setuptools.Distribution) -> None: """Helper to call version_keyword with calver-by-date scheme and return the result.""" - from setuptools_scm._integration.setuptools import version_keyword - version_keyword(dist, "use_scm_version", {"version_scheme": "calver-by-date"}) - return "version_keyword_calver" + setuptools_integration.version_keyword( + dist, "use_scm_version", {"version_scheme": "calver-by-date"} + ) # 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 - (integration_infer_version, integration_version_keyword_default, "1.0.1.dev1"), + (setuptools_integration.infer_version, version_keyword_default, "1.0.1.dev1"), ( - integration_infer_version, - integration_version_keyword_calver, + setuptools_integration.infer_version, + version_keyword_calver, "9.2.13.0.dev1", ), # calver should win but doesn't - (integration_version_keyword_default, integration_infer_version, "1.0.1.dev1"), - (integration_version_keyword_calver, integration_infer_version, "9.2.13.0.dev1"), + (version_keyword_default, setuptools_integration.infer_version, "1.0.1.dev1"), + (version_keyword_calver, setuptools_integration.infer_version, "9.2.13.0.dev1"), ] @@ -1010,11 +1000,6 @@ def test_integration_function_call_order( wd.commit_testfile("test2") # Add another commit to get distance monkeypatch.chdir(wd.cwd) - # Generate unique distribution name based on the test combination - first_name = first_integration.__name__.replace("integration_", "") - second_name = second_integration.__name__.replace("integration_", "") - dist_name = f"test-pkg-{first_name}-then-{second_name}" - # Create a pyproject.toml file pyproject_content = f""" [build-system] @@ -1022,7 +1007,7 @@ def test_integration_function_call_order( build-backend = "setuptools.build_meta" [project] -name = "{dist_name}" +name = "test-pkg-{first_integration.__name__}-{second_integration.__name__}" dynamic = ["version"] [tool.setuptools_scm] @@ -1030,11 +1015,9 @@ def test_integration_function_call_order( """ wd.write("pyproject.toml", pyproject_content) - import setuptools - - # Create distribution and clear any auto-set version - dist = setuptools.Distribution({"name": dist_name}) - dist.metadata.version = None + dist = create_clean_distribution( + f"test-pkg-{first_integration.__name__}-{second_integration.__name__}" + ) # Call both integration functions in order first_integration(dist) From e5223146d0ed269033ed7f6ea08747ebfb2d3922 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 11 Aug 2025 11:55:55 +0200 Subject: [PATCH 103/162] add changelog --- CHANGELOG.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d77e15ad..db7cb4a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,30 @@ # Changelog -## v9.0.3 +## v9.1.0 + +### fixed + +- complete reiteration of the decision logic for enabling version inference on setuptools_scm + + - shared logic for the important parts + - proper deferring based in precedence of finalize options vs version keyword + - unittestable for the parsing parts and the decision steps + + +## v9.0.3 (yanked) ### fixed - fix 1184: verify version is dynamic if the dependency is used as indicator for enabling -## v9.0.2 +## v9.0.2 (yanked) ### Fixed - fix #1184: in case setuptools-scm is a indirect dependency and no pyproject.toml section exists - don't infer the version -## v9.0.1 +## v9.0.1 (yanked) ### Fixed @@ -22,7 +33,7 @@ as it turns out there's valid use cases where setuptools_scm is not direct part of the dependencies - add codespell pre-commit hook -## v9.0.0 +## v9.0.0 (yanked) ### Breaking From 90b9dc5e9124d79e92b695b8bd72134f42909809 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 11 Aug 2025 15:37:14 +0200 Subject: [PATCH 104/162] chore: don't log long description of a distribution --- src/setuptools_scm/_integration/setuptools.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 6a98656a..9c9733e7 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -38,7 +38,13 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None: - log.debug("%s %s %s %r", hook, id(dist), id(dist.metadata), vars(dist.metadata)) + log.debug( + "%s %s %s %r", + hook, + id(dist), + id(dist.metadata), + {**vars(dist.metadata), "long_description": ...}, + ) def get_keyword_overrides( From 0eecd084af723236f206abfde6b0246c0191d12d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 11 Aug 2025 15:38:09 +0200 Subject: [PATCH 105/162] no longer require tool secrion when the version keyword is used closes #1194 --- .../_integration/version_inference.py | 5 +- testing/test_integration.py | 35 +++++++++++++ testing/test_version_inference.py | 52 +++++++++++++++++-- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index 5fd4991e..51f42cd5 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -154,13 +154,15 @@ def get_version_inference_config( # Handle missing configuration if not pyproject_data.is_required and not pyproject_data.section_present: - # If there are overrides, proceed with inference (explicit use_scm_version) + # 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 @@ -168,6 +170,7 @@ def get_version_inference_config( 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() diff --git a/testing/test_integration.py b/testing/test_integration.py index 89dc9cda..467cbdf7 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1305,3 +1305,38 @@ def test_infer_version_logs_debug_when_missing_dynamic_version( # 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 +) -> None: + """Test that pip download works for xmlsec package without causing setuptools_scm regression. + + This test ensures that downloading and building xmlsec from source doesn't fail + due to setuptools_scm issues when using --no-build-isolation. + """ + # Set up environment with setuptools_scm debug enabled + monkeypatch.setenv("SETUPTOOLS_SCM_DEBUG", "1") + monkeypatch.setenv("COLUMNS", "150") + + # Run pip download command with no-binary and no-build-isolation + try: + subprocess.run( + [ + *(sys.executable, "-m", "pip", "download"), + *("--no-binary", "xmlsec"), + "--no-build-isolation", + "-v", + "xmlsec==1.3.16", + ], + cwd=tmp_path, + text=True, + timeout=300, + check=True, + ) + except subprocess.CalledProcessError as e: + pytest.fail(f"pip download failed: {e}", pytrace=False) + + # The success of the subprocess.run call above means the regression is fixed. + # pip download succeeded without setuptools_scm causing version conflicts. diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index d6911358..5bdd2861 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -81,26 +81,70 @@ def test_no_pyproject_toml(self) -> None: # 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(self) -> None: - """Test that we don't infer when setuptools-scm is not configured.""" + 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 ) assert isinstance(result, VersionInferenceNoOp) - def test_setuptools_scm_required_no_project_section(self) -> None: - """Test that we don't infer when setuptools-scm is required but no project section.""" + 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 == {} + + 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", current_version=None, pyproject_data=PyProjectData.for_testing(True, False, False), + overrides=None, # infer_version call ) 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", + current_version=None, + pyproject_data=PyProjectData.for_testing(True, False, False), + overrides={}, # version_keyword call with use_scm_version=True + ) + + 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", + current_version=None, + pyproject_data=PyProjectData.for_testing(True, False, False), + overrides=overrides, # version_keyword call with use_scm_version={config} + ) + + 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( From 0ef48be23776cabd5b33ccce60fd3e8096698115 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 11 Aug 2025 15:38:57 +0200 Subject: [PATCH 106/162] chore: upload release artifacts as assets --- .github/workflows/python-tests.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 89815baa..37a03761 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -121,6 +121,23 @@ jobs: - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + upload-release-assets: + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + needs: [test] + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - name: Upload release assets + uses: softprops/action-gh-release@v2 + with: + files: dist/* + fail_on_unmatched_files: true + test-pypi-upload: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' From a4b755e74d082623da9af08a26d7f211e9958a13 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 11 Aug 2025 15:40:37 +0200 Subject: [PATCH 107/162] changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d77e15ad..673cf02b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v9.1.1 + +### fixed + +- fix #1194: correctly handle version keyword when pyproject metadata is missing + + ## v9.0.3 ### fixed From 08c2e289f40fcc52553d209a0eb43c182ffc0911 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:44:01 +0000 Subject: [PATCH 108/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) - [github.com/astral-sh/ruff-pre-commit: v0.12.7 → v0.12.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.7...v0.12.8) - [github.com/codespell-project/codespell: v2.3.0 → v2.4.1](https://github.com/codespell-project/codespell/compare/v2.3.0...v2.4.1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7c3dfd0b..a2d32f12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: check-yaml - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.12.8 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix, --show-fixes] @@ -33,7 +33,7 @@ repos: - id: sp-repo-review - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell args: [-w, --ignore-words-list=hist,nd,te] From 0d96cd0f8f1e19ecf331eb0203f552f80c42a8d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 00:16:15 +0000 Subject: [PATCH 109/162] Bump actions/download-artifact from 4 to 5 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/python-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 37a03761..910655ee 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -92,7 +92,7 @@ jobs: git config --system gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe" if: runner.os == 'Windows' - run: uv sync --group test --group docs --extra rich - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: Packages path: dist @@ -114,7 +114,7 @@ jobs: id-token: write needs: [test] steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: Packages path: dist @@ -128,7 +128,7 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: Packages path: dist @@ -145,7 +145,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: Packages path: dist From 3f3927ca81493f5199294b8b60595a40ba7edd22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 01:11:11 +0000 Subject: [PATCH 110/162] Bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/api-check.yml | 2 +- .github/workflows/python-tests.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml index 18b7d4ed..c0066353 100644 --- a/.github/workflows/api-check.yml +++ b/.github/workflows/api-check.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 37a03761..1f45bb0f 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -27,7 +27,7 @@ jobs: SETUPTOOLS_SCM_NO_LOCAL: "1" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 @@ -50,7 +50,7 @@ jobs: name: ${{ matrix.os }} - Python ${{ matrix.python_version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Setup python From dfadacc34b0e11ac2379ed4f7a6ee61b317f5d29 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 08:27:00 +0200 Subject: [PATCH 111/162] Refactor version inference logic to use should_infer method for dynamic version verification - Introduced should_infer method in PyProjectData to determine if version inference should proceed based on configuration. - Updated version_inference.py to utilize should_infer for handling version inference conditions. - Modified tests to reflect changes in dynamic version verification and ensure proper error handling when dynamic=['version'] is missing. --- .../_integration/pyproject_reading.py | 63 +++++++++++++------ .../_integration/version_inference.py | 45 ++++++------- testing/test_integration.py | 17 +++-- 3 files changed, 77 insertions(+), 48 deletions(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index df5d30c8..44d66425 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -33,13 +33,20 @@ def for_testing( section_present: bool = False, project_present: bool = False, project_name: str | None = None, + has_dynamic_version: bool = True, ) -> 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"] + return cls( path=Path("pyproject.toml"), tool_name="setuptools_scm", @@ -54,22 +61,43 @@ def for_testing( 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." - ) + def should_infer(self) -> bool: + """ + Determine if setuptools_scm should infer version based on configuration. + + This method only considers the pyproject.toml configuration state. + It does not consider version_keyword context (overrides always infer). + + Returns: + True if version inference should proceed based on configuration + + Raises: + ValueError: If setuptools-scm is required but dynamic=['version'] is missing + """ + # If there's a tool section, always infer + if self.section_present: + return True + + # If not required, don't auto-activate for infer_version + if not self.is_required: + return False + + # setuptools-scm is required but no tool section + if not self.project_present: + # No project section - don't auto-activate + return False + + # Project section exists - check for dynamic=['version'] + 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." + ) + + # All conditions met + return True def has_build_package( @@ -134,9 +162,6 @@ def read_pyproject( path, tool_name, project, section, is_required, section_present, project_present ) - # 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/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index 51f42cd5..c1b61d10 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -152,31 +152,26 @@ def get_version_inference_config( 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() + # version_keyword (with overrides) always tries to infer + if overrides is not None: + return VersionInferenceConfig( + dist_name=dist_name, + pyproject_data=pyproject_data, + overrides=overrides, + ) - # 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 - ): + # infer_version (no overrides) uses pyproject configuration to decide + try: + should_proceed = pyproject_data.should_infer() + except ValueError: + # For infer_version, silently skip on configuration issues (auto-activation shouldn't error) return VersionInferenceNoOp() - # All conditions met - proceed with inference - return VersionInferenceConfig( - dist_name=dist_name, - pyproject_data=pyproject_data, - overrides=overrides, - ) + if should_proceed: + return VersionInferenceConfig( + dist_name=dist_name, + pyproject_data=pyproject_data, + overrides=overrides, + ) + else: + return VersionInferenceNoOp() diff --git a/testing/test_integration.py b/testing/test_integration.py index 467cbdf7..4f617871 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1181,7 +1181,7 @@ def test_version_keyword_no_scm_dependency_works( 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""" + """Test that should_infer raises ValueError when setuptools-scm is in build-system.requires but dynamic=['version'] is missing""" if sys.version_info < (3, 11): pytest.importorskip("tomli") @@ -1202,11 +1202,14 @@ def test_verify_dynamic_version_when_required_missing_dynamic( from setuptools_scm._integration.pyproject_reading import read_pyproject - # This should raise a ValueError because dynamic=['version'] is missing + # Read pyproject data first + pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True) + + # should_infer should raise a ValueError when 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) + pyproject_data.should_infer() def test_verify_dynamic_version_when_required_with_tool_section( @@ -1240,6 +1243,9 @@ def test_verify_dynamic_version_when_required_with_tool_section( assert pyproject_data.is_required is True assert pyproject_data.section_present is True + # should_infer should return True because tool section exists + assert pyproject_data.should_infer() is True + def test_verify_dynamic_version_when_required_with_dynamic( wd: WorkDir, monkeypatch: pytest.MonkeyPatch @@ -1270,11 +1276,14 @@ def test_verify_dynamic_version_when_required_with_dynamic( assert pyproject_data.is_required is True assert pyproject_data.section_present is False + # should_infer should return True because dynamic=['version'] is set + assert pyproject_data.should_infer() is True + 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""" + """Test that infer_version gracefully handles and 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") From 5cd6d2ba4eca6a6a7934945c41183d7c67654b99 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 08:36:35 +0200 Subject: [PATCH 112/162] use PyProjectData.for_testing instead of writing to files in tests where appropiate --- testing/test_integration.py | 108 +++++++++++------------------------- 1 file changed, 33 insertions(+), 75 deletions(-) diff --git a/testing/test_integration.py b/testing/test_integration.py index 4f617871..72338214 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1178,32 +1178,18 @@ def test_version_keyword_no_scm_dependency_works( assert dist.metadata.version == "1.0.0" -def test_verify_dynamic_version_when_required_missing_dynamic( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_verify_dynamic_version_when_required_missing_dynamic() -> None: """Test that should_infer raises ValueError 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 - - # Read pyproject data first - pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True) + from setuptools_scm._integration.pyproject_reading import PyProjectData + + # Create pyproject data: setuptools-scm required, project present, but no dynamic=['version'] + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + project_name="test-package-missing-dynamic", + has_dynamic_version=False, # This is the key: no dynamic=['version'] + ) # should_infer should raise a ValueError when dynamic=['version'] is missing with pytest.raises( @@ -1212,34 +1198,19 @@ def test_verify_dynamic_version_when_required_missing_dynamic( pyproject_data.should_infer() -def test_verify_dynamic_version_when_required_with_tool_section( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_verify_dynamic_version_when_required_with_tool_section() -> 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 + from setuptools_scm._integration.pyproject_reading import PyProjectData + + # Create pyproject data: setuptools-scm required, tool section present + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=True, # This is the key: tool section exists + project_present=True, + project_name="test-package-with-tool-section", + has_dynamic_version=False, # dynamic=['version'] not needed when tool section exists + ) - # 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 @@ -1247,32 +1218,19 @@ def test_verify_dynamic_version_when_required_with_tool_section( assert pyproject_data.should_infer() is True -def test_verify_dynamic_version_when_required_with_dynamic( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_verify_dynamic_version_when_required_with_dynamic() -> 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 + from setuptools_scm._integration.pyproject_reading import PyProjectData + + # Create pyproject data: setuptools-scm required, no tool section, but dynamic=['version'] set + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + project_name="test-package-with-dynamic", + has_dynamic_version=True, # This is the key: dynamic=['version'] is set + ) - # 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 From 9f6796e2e8408a9d2cae2ff0498f7c925b4b9794 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 08:53:09 +0200 Subject: [PATCH 113/162] enhance testability of setuptools integration points - enable dependency injecting supposed pyporject data - migrate tests to ue the api directly instead of writing --- src/setuptools_scm/_integration/setuptools.py | 41 +++-- testing/test_integration.py | 141 ++++++++---------- 2 files changed, 87 insertions(+), 95 deletions(-) diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 9c9733e7..be93d69f 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -8,6 +8,7 @@ import setuptools +from .pyproject_reading import PyProjectData from .pyproject_reading import read_pyproject from .setup_cfg import _dist_name_from_legacy from .version_inference import get_version_inference_config @@ -64,6 +65,8 @@ def version_keyword( dist: setuptools.Distribution, keyword: str, value: bool | dict[str, Any] | Callable[[], dict[str, Any]], + *, + _given_pyproject_data: PyProjectData | None = None, ) -> None: """apply version infernce when setup(use_scm_version=...) is used this takes priority over the finalize_options based version @@ -83,11 +86,16 @@ def version_keyword( was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) # Get pyproject data - try: - pyproject_data = read_pyproject(missing_section_ok=True, missing_file_ok=True) - except (LookupError, ValueError) as e: - log.debug("Configuration issue in pyproject.toml: %s", e) - return + if _given_pyproject_data is not None: + pyproject_data = _given_pyproject_data + else: + try: + pyproject_data = read_pyproject( + missing_section_ok=True, missing_file_ok=True + ) + except (LookupError, ValueError) as e: + log.debug("Configuration issue in pyproject.toml: %s", e) + return result = get_version_inference_config( dist_name=dist_name, @@ -100,7 +108,9 @@ def version_keyword( result.apply(dist) -def infer_version(dist: setuptools.Distribution) -> None: +def infer_version( + dist: setuptools.Distribution, *, _given_pyproject_data: PyProjectData | None = None +) -> 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 @@ -112,14 +122,17 @@ def infer_version(dist: setuptools.Distribution) -> None: dist_name = _dist_name_from_legacy(dist) - try: - pyproject_data = read_pyproject(missing_section_ok=True) - except FileNotFoundError: - log.debug("pyproject.toml not found, skipping infer_version") - return - except (LookupError, ValueError) as e: - log.debug("Configuration issue in pyproject.toml: %s", e) - return + if _given_pyproject_data is not None: + pyproject_data = _given_pyproject_data + else: + try: + pyproject_data = read_pyproject(missing_section_ok=True) + except FileNotFoundError: + log.debug("pyproject.toml not found, skipping infer_version") + return + except (LookupError, ValueError) as e: + log.debug("Configuration issue in pyproject.toml: %s", e) + return result = get_version_inference_config( dist_name=dist_name, diff --git a/testing/test_integration.py b/testing/test_integration.py index 72338214..145067c7 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1037,33 +1037,28 @@ 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.pyproject_reading import PyProjectData from setuptools_scm._integration.setuptools import infer_version + # Create pyproject data: setuptools_scm required, no tool section, project with dynamic=['version'] + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + project_name="test-package-infer-version", + has_dynamic_version=True, + ) + # 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) + # Call infer_version with direct data injection - no file I/O! + infer_version(dist, _given_pyproject_data=pyproject_data) # Verify that version was set assert dist.metadata.version is not None @@ -1077,33 +1072,28 @@ 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.pyproject_reading import PyProjectData from setuptools_scm._integration.setuptools import infer_version + # Create pyproject data: setuptools-scm required, no tool section, project with dynamic=['version'] + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + project_name="test-package-infer-version-dash", + has_dynamic_version=True, + ) + # 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) + # Call infer_version with direct data injection - no file I/O! + infer_version(dist, _given_pyproject_data=pyproject_data) # Verify that version was set assert dist.metadata.version is not None @@ -1117,32 +1107,28 @@ 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.pyproject_reading import PyProjectData from setuptools_scm._integration.setuptools import infer_version + # Create pyproject data: setuptools-scm NOT required, no tool section, project with dynamic=['version'] + pyproject_data = PyProjectData.for_testing( + is_required=False, # This is the key: NOT in build-system.requires + section_present=False, + project_present=True, + project_name="test-package-no-scm", + has_dynamic_version=True, + ) + # Create clean distribution dist = create_clean_distribution("test-package-no-scm") - infer_version(dist) + # Call infer_version with direct data injection - should silently return + infer_version(dist, _given_pyproject_data=pyproject_data) assert dist.metadata.version is None @@ -1154,27 +1140,25 @@ def test_version_keyword_no_scm_dependency_works( 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.pyproject_reading import PyProjectData from setuptools_scm._integration.setuptools import version_keyword + # Create pyproject data: setuptools-scm NOT required, no tool section, project with dynamic=['version'] + pyproject_data = PyProjectData.for_testing( + is_required=False, # This is the key: NOT in build-system.requires + section_present=False, + project_present=True, + project_name="test-package-no-scm", + has_dynamic_version=True, + ) + # Create distribution dist = setuptools.Distribution({"name": "test-package-no-scm"}) - version_keyword(dist, "use_scm_version", True) + # Call version_keyword with direct data injection - should work regardless of config + version_keyword(dist, "use_scm_version", True, _given_pyproject_data=pyproject_data) assert dist.metadata.version == "1.0.0" @@ -1242,33 +1226,28 @@ def test_infer_version_logs_debug_when_missing_dynamic_version( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that infer_version gracefully handles and 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.pyproject_reading import PyProjectData from setuptools_scm._integration.setuptools import infer_version + # Create pyproject data: setuptools-scm required, project present, but no dynamic=['version'] + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + project_name="test-package-missing-dynamic", + has_dynamic_version=False, # This is the key: missing dynamic=['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) + # This should not raise an error, but should silently return (the configuration issue is handled internally) + infer_version(dist, _given_pyproject_data=pyproject_data) # Verify that version was not set due to configuration issue assert dist.metadata.version is None From 9d4e67eb92c3fb89fe5721a9189f8fdb8ea10e3e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 09:06:43 +0200 Subject: [PATCH 114/162] remove the missing section km parameter --- src/setuptools_scm/_config.py | 8 +----- .../_integration/pyproject_reading.py | 21 ++++----------- src/setuptools_scm/_integration/setuptools.py | 6 ++--- .../_integration/version_inference.py | 1 - testing/test_integration.py | 26 +++++++++---------- 5 files changed, 21 insertions(+), 41 deletions(-) diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 7c1d185a..291337de 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -271,30 +271,24 @@ def from_file( 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, ) args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 44d66425..fbf11098 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -114,7 +114,6 @@ def read_pyproject( path: Path = Path("pyproject.toml"), tool_name: str = "setuptools_scm", canonical_build_package_name: str = "setuptools-scm", - missing_section_ok: bool = False, missing_file_ok: bool = False, ) -> PyProjectData: try: @@ -140,21 +139,11 @@ def read_pyproject( 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 + except LookupError: + 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 project = defn.get("project", {}) project_present = "project" in defn diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index be93d69f..c3fd57ac 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -90,9 +90,7 @@ def version_keyword( pyproject_data = _given_pyproject_data else: try: - pyproject_data = read_pyproject( - missing_section_ok=True, missing_file_ok=True - ) + pyproject_data = read_pyproject(missing_file_ok=True) except (LookupError, ValueError) as e: log.debug("Configuration issue in pyproject.toml: %s", e) return @@ -126,7 +124,7 @@ def infer_version( pyproject_data = _given_pyproject_data else: try: - pyproject_data = read_pyproject(missing_section_ok=True) + pyproject_data = read_pyproject() except FileNotFoundError: log.debug("pyproject.toml not found, skipping infer_version") return diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index c1b61d10..3fe1692d 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -38,7 +38,6 @@ def apply(self, dist: Any) -> None: dist_name=self.dist_name, pyproject_data=self.pyproject_data, missing_file_ok=True, - missing_section_ok=True, **(self.overrides or {}), ) diff --git a/testing/test_integration.py b/testing/test_integration.py index 145067c7..a25b0583 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -881,10 +881,10 @@ def test_build_requires_integration_with_config_reading(wd: WorkDir) -> None: assert config.dist_name == "test-package" -def test_improved_error_message_mentions_both_config_options( +def test_missing_section_no_longer_raises_error( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: - """Test that the error message mentions both configuration options""" + """Test that missing [tool.setuptools_scm] section no longer raises error, creates valid config""" if sys.version_info < (3, 11): pytest.importorskip("tomli") @@ -904,18 +904,18 @@ def test_improved_error_message_mentions_both_config_options( 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, - ) + # This should no longer raise an error - instead it should create a valid configuration + # with default values and log a warning + config = 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 + # Should have created a valid configuration with default values + assert config.dist_name == "test-package" + assert config.version_scheme == "guess-next-dev" # default + assert config.local_scheme == "node-and-date" # default # Helper function for creating and managing distribution objects From 2319a9db4bb8293ed6820cab2edddedfbac17019 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 09:15:01 +0200 Subject: [PATCH 115/162] migrate test_integration_function_call_order to direct pyproject data injection --- testing/test_integration.py | 84 ++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/testing/test_integration.py b/testing/test_integration.py index a25b0583..82780da4 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -17,6 +17,7 @@ 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._requirement_cls import extract_package_name if TYPE_CHECKING: @@ -939,33 +940,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 +978,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 +1001,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 +1011,23 @@ 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( + path=Path("pyproject.toml"), + tool_name="setuptools_scm", + project={"name": project_name, "dynamic": ["version"]}, + section={"local_scheme": "no-local-version"}, # [tool.setuptools_scm] config + is_required=True, # setuptools_scm in build-system.requires + section_present=True, # [tool.setuptools_scm] section exists + project_present=True, # [project] section exists ) - # 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 From 869ae51e31e74b34c23760874f6169d0b7f89658 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 09:20:19 +0200 Subject: [PATCH 116/162] Enhance pytest report header to format package paths for better readability - Updated the path representation in the pytest report header to replace 'site-packages' with 'site:.' and the current working directory with 'CWD:.' for improved clarity. --- testing/conftest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 From 6c335cd9e1c97ea2960764cef7832773ef176f42 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 09:33:12 +0200 Subject: [PATCH 117/162] Refactor PyProjectData initialization and improve error handling for missing sections - Introduced a class method `empty` in PyProjectData for creating empty instances. - Simplified the `read_pyproject` function to utilize the new `empty` method when the configuration file is missing. - Enhanced logging for missing tool sections in the TOML configuration. --- .../_integration/pyproject_reading.py | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index fbf11098..cca5808a 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -57,6 +57,18 @@ def for_testing( project_present=project_present, ) + @classmethod + def empty(cls, path: Path, tool_name: str) -> PyProjectData: + return cls( + path=path, + tool_name=tool_name, + project={}, + section={}, + is_required=False, + section_present=False, + project_present=False, + ) + @property def project_name(self) -> str | None: return self.project.get("name") @@ -121,29 +133,23 @@ def read_pyproject( 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, - ) + return PyProjectData.empty(path=path, tool_name=tool_name) else: raise 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: - 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 From c2f7caf745fb20035dbbd26b642a6a14f78891df Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 09:51:37 +0200 Subject: [PATCH 118/162] remove the missing_file_ok hack and use exception handling instead --- src/setuptools_scm/_config.py | 7 +------ .../_integration/pyproject_reading.py | 7 +------ src/setuptools_scm/_integration/setuptools.py | 8 ++++++- .../_integration/version_inference.py | 1 - testing/test_integration.py | 1 - testing/test_pyproject_reading.py | 21 +++---------------- 6 files changed, 12 insertions(+), 33 deletions(-) diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 291337de..81a78e84 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -270,7 +270,6 @@ def from_file( cls, name: str | os.PathLike[str] = "pyproject.toml", dist_name: str | None = None, - missing_file_ok: bool = False, pyproject_data: PyProjectData | None = None, **kwargs: Any, ) -> Configuration: @@ -282,15 +281,11 @@ def from_file( 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 - **kwargs: additional keyword arguments to pass to the Configuration constructor """ if pyproject_data is None: - pyproject_data = _read_pyproject( - Path(name), - 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 cca5808a..a2ec65f1 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -126,16 +126,11 @@ def read_pyproject( path: Path = Path("pyproject.toml"), tool_name: str = "setuptools_scm", canonical_build_package_name: str = "setuptools-scm", - missing_file_ok: bool = False, ) -> 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.empty(path=path, tool_name=tool_name) - else: - raise + raise requires: list[str] = defn.get("build-system", {}).get("requires", []) is_required = has_build_package(requires, canonical_build_package_name) diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index c3fd57ac..bb3dfc21 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -3,6 +3,7 @@ import logging import warnings +from pathlib import Path from typing import Any from typing import Callable @@ -90,7 +91,12 @@ def version_keyword( pyproject_data = _given_pyproject_data else: try: - pyproject_data = read_pyproject(missing_file_ok=True) + pyproject_data = read_pyproject() + except FileNotFoundError: + log.debug("pyproject.toml not found, proceeding with empty configuration") + pyproject_data = PyProjectData.empty( + Path("pyproject.toml"), "setuptools_scm" + ) except (LookupError, ValueError) as e: log.debug("Configuration issue in pyproject.toml: %s", e) return diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index 3fe1692d..30b0d974 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -37,7 +37,6 @@ def apply(self, dist: Any) -> None: config = _config_module.Configuration.from_file( dist_name=self.dist_name, pyproject_data=self.pyproject_data, - missing_file_ok=True, **(self.overrides or {}), ) diff --git a/testing/test_integration.py b/testing/test_integration.py index 82780da4..12882059 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -910,7 +910,6 @@ def test_missing_section_no_longer_raises_error( config = Configuration.from_file( name=wd.cwd.joinpath("pyproject.toml"), dist_name="test-package", - missing_file_ok=False, ) # Should have created a valid configuration with default values diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py index 592adf86..bd906bd8 100644 --- a/testing/test_pyproject_reading.py +++ b/testing/test_pyproject_reading.py @@ -10,25 +10,10 @@ 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.""" From b3c34c0de983ad4138f37a634834c10c4231939e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 10:15:43 +0200 Subject: [PATCH 119/162] Refactor TOML error handling and improve pyproject reading logic - Introduced `InvalidTomlError` to handle parsing errors in TOML files. - Simplified the `read_pyproject` function by removing unnecessary exception handling for file not found. - Updated exception handling in version inference to use `InvalidTomlError` for better clarity in error logging. --- .../_integration/pyproject_reading.py | 5 +---- src/setuptools_scm/_integration/setuptools.py | 5 +++-- src/setuptools_scm/_integration/toml.py | 22 ++++++++++++++----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index a2ec65f1..4c697952 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -127,10 +127,7 @@ def read_pyproject( tool_name: str = "setuptools_scm", canonical_build_package_name: str = "setuptools-scm", ) -> PyProjectData: - try: - defn = read_toml_content(path) - except FileNotFoundError: - raise + defn = read_toml_content(path) requires: list[str] = defn.get("build-system", {}).get("requires", []) is_required = has_build_package(requires, canonical_build_package_name) diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index bb3dfc21..22a47d25 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -12,6 +12,7 @@ from .pyproject_reading import PyProjectData from .pyproject_reading import read_pyproject from .setup_cfg import _dist_name_from_legacy +from .toml import InvalidTomlError from .version_inference import get_version_inference_config log = logging.getLogger(__name__) @@ -97,7 +98,7 @@ def version_keyword( pyproject_data = PyProjectData.empty( Path("pyproject.toml"), "setuptools_scm" ) - except (LookupError, ValueError) as e: + except InvalidTomlError as e: log.debug("Configuration issue in pyproject.toml: %s", e) return @@ -134,7 +135,7 @@ def infer_version( 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 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 From 882ef632352d4db70e05f0cdbaae787f2698bc43 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 15:29:25 +0200 Subject: [PATCH 120/162] Enhance testing capabilities for pyproject reading and version inference - Introduced `_given_result` parameter in `read_pyproject` - expand the setuptools integration points with it - Improved error handling by allowing direct injection of `PyProjectData`, `InvalidTomlError`, or `FileNotFoundError` for better testability. --- .../_integration/pyproject_reading.py | 23 +++++++++ src/setuptools_scm/_integration/setuptools.py | 49 +++++++++---------- src/setuptools_scm/_types.py | 7 +++ 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 4c697952..8583d941 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") @@ -126,7 +128,28 @@ def read_pyproject( path: Path = Path("pyproject.toml"), tool_name: str = "setuptools_scm", canonical_build_package_name: str = "setuptools-scm", + _given_result: _t.GivenPyProjectResult = None, ) -> PyProjectData: + """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", []) diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 22a47d25..5283c0d5 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -9,6 +9,7 @@ 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 @@ -68,7 +69,7 @@ def version_keyword( keyword: str, value: bool | dict[str, Any] | Callable[[], dict[str, Any]], *, - _given_pyproject_data: PyProjectData | None = None, + _given_pyproject_data: _t.GivenPyProjectResult = None, ) -> None: """apply version infernce when setup(use_scm_version=...) is used this takes priority over the finalize_options based version @@ -87,20 +88,15 @@ def version_keyword( was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) - # Get pyproject data - if _given_pyproject_data is not None: - pyproject_data = _given_pyproject_data - else: - try: - pyproject_data = read_pyproject() - except FileNotFoundError: - log.debug("pyproject.toml not found, proceeding with empty configuration") - pyproject_data = PyProjectData.empty( - Path("pyproject.toml"), "setuptools_scm" - ) - except InvalidTomlError as e: - log.debug("Configuration issue in pyproject.toml: %s", e) - return + # Get pyproject data (support direct injection for tests) + try: + 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(Path("pyproject.toml"), "setuptools_scm") + except InvalidTomlError as e: + log.debug("Configuration issue in pyproject.toml: %s", e) + return result = get_version_inference_config( dist_name=dist_name, @@ -114,7 +110,9 @@ def version_keyword( def infer_version( - dist: setuptools.Distribution, *, _given_pyproject_data: PyProjectData | None = None + dist: setuptools.Distribution, + *, + _given_pyproject_data: _t.GivenPyProjectResult = None, ) -> 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 @@ -127,17 +125,14 @@ def infer_version( dist_name = _dist_name_from_legacy(dist) - if _given_pyproject_data is not None: - pyproject_data = _given_pyproject_data - else: - try: - pyproject_data = read_pyproject() - except FileNotFoundError: - log.debug("pyproject.toml not found, skipping infer_version") - return - except InvalidTomlError as e: - log.debug("Configuration issue in pyproject.toml: %s", e) - return + try: + pyproject_data = read_pyproject(_given_result=_given_pyproject_data) + except FileNotFoundError: + log.debug("pyproject.toml not found, skipping infer_version") + return + except InvalidTomlError as e: + log.debug("Configuration issue in pyproject.toml: %s", e) + return result = get_version_inference_config( dist_name=dist_name, diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index 6cc4e774..703636f5 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -18,6 +18,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 +31,8 @@ # 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 +] From 3eb0d458bd6239d795be1c7e2adb0e54e39d7ab3 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 15:44:12 +0200 Subject: [PATCH 121/162] simplify version inference logic tree --- .../_integration/version_inference.py | 69 ++++++++----------- testing/test_version_inference.py | 17 ++--- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index 30b0d974..8bd12d6b 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -114,62 +114,49 @@ def get_version_inference_config( Returns: VersionInferenceResult with the decision and configuration """ + # Normalize name from project metadata when not provided if dist_name is None: dist_name = pyproject_data.project_name - # Handle version already set + # If a version is already present, decide based on context (infer_version vs version_keyword) 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 + # infer_version call (overrides is None) should be a no-op if version already exists + if overrides is None: + return VersionInferenceNoOp() + + if not was_set_by_infer: return VersionInferenceError( - f"version of {dist_name} already set", should_warn=True + f"version of {dist_name} already set", + should_warn=pyproject_data.should_infer(), ) - # Handle setuptools-scm package + # Version was set by infer_version previously + if overrides: + # Non-empty overrides from version_keyword → re-infer with overrides + return VersionInferenceConfig( + dist_name=dist_name, pyproject_data=pyproject_data, overrides=overrides + ) + # Empty overrides dict from version_keyword → keep existing version + return VersionInferenceNoOp() + + # Do not infer a version for setuptools-scm itself if dist_name == "setuptools-scm": return VersionInferenceNoOp() - # version_keyword (with overrides) always tries to infer + # version_keyword path: any overrides (empty or not) mean we should infer if overrides is not None: return VersionInferenceConfig( - dist_name=dist_name, - pyproject_data=pyproject_data, - overrides=overrides, + dist_name=dist_name, pyproject_data=pyproject_data, overrides=overrides ) - # infer_version (no overrides) uses pyproject configuration to decide + # infer_version path: decide based on pyproject configuration only try: - should_proceed = pyproject_data.should_infer() + if pyproject_data.should_infer(): + return VersionInferenceConfig( + dist_name=dist_name, pyproject_data=pyproject_data, overrides=None + ) except ValueError: - # For infer_version, silently skip on configuration issues (auto-activation shouldn't error) + # Auto-activation should not error in infer_version context → skip silently return VersionInferenceNoOp() - if should_proceed: - return VersionInferenceConfig( - dist_name=dist_name, - pyproject_data=pyproject_data, - overrides=overrides, - ) - else: - return VersionInferenceNoOp() + return VersionInferenceNoOp() diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index 5bdd2861..e1296aac 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -26,7 +26,7 @@ def test_version_already_set_by_infer_with_overrides(self) -> None: 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).""" + """infer_version call with existing version should be a no-op.""" result = get_version_inference_config( dist_name="test_package", current_version="1.0.0", @@ -35,9 +35,7 @@ def test_version_already_set_by_infer_no_overrides(self) -> None: was_set_by_infer=True, ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - assert result.overrides is None + assert isinstance(result, VersionInferenceNoOp) 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).""" @@ -52,7 +50,7 @@ def test_version_already_set_by_infer_empty_overrides(self) -> None: 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.""" + """infer_version call with existing version set by something else should be a no-op.""" result = get_version_inference_config( dist_name="test_package", current_version="1.0.0", @@ -61,9 +59,7 @@ def test_version_already_set_by_something_else(self) -> 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 + assert isinstance(result, VersionInferenceNoOp) def test_setuptools_scm_package(self) -> None: """Test that we don't infer for setuptools-scm package itself.""" @@ -190,7 +186,7 @@ def test_none_dist_name(self) -> None: 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.""" + """infer_version call with None dist_name and existing version should be a no-op.""" result = get_version_inference_config( dist_name=None, current_version="1.0.0", @@ -199,8 +195,7 @@ def test_version_already_set_none_dist_name(self) -> None: was_set_by_infer=False, ) - assert isinstance(result, VersionInferenceError) - assert result.message == "version of None already set" + assert isinstance(result, VersionInferenceNoOp) def test_overrides_passed_through(self) -> None: """Test that overrides are passed through to the config.""" From 02cf5b0a588ed9274f149b9d93c3f09015253120 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 15:54:20 +0200 Subject: [PATCH 122/162] add ruff/mypy back to the test --- pyproject.toml | 2 ++ uv.lock | 85 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 78b8f437..8fbd0dbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,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/uv.lock b/uv.lock index e28d70b0..996e30c3 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" }, @@ -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" }, ] From f1ccfef267f35983458019d2ee14cd07c55d68ae Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 15:54:38 +0200 Subject: [PATCH 123/162] chore: pyproject_reading: add constants --- .../_integration/pyproject_reading.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 8583d941..6b5f4531 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -18,6 +18,10 @@ _ROOT = "root" +DEFAULT_PYPROJECT_PATH = Path("pyproject.toml") +DEFAULT_TOOL_NAME = "setuptools_scm" + + @dataclass class PyProjectData: path: Path @@ -50,8 +54,8 @@ def for_testing( project["dynamic"] = ["version"] return cls( - path=Path("pyproject.toml"), - tool_name="setuptools_scm", + path=DEFAULT_PYPROJECT_PATH, + tool_name=DEFAULT_TOOL_NAME, project=project, section={}, is_required=is_required, @@ -60,7 +64,9 @@ def for_testing( ) @classmethod - def empty(cls, path: Path, tool_name: str) -> PyProjectData: + def empty( + cls, path: Path = DEFAULT_PYPROJECT_PATH, tool_name: str = DEFAULT_TOOL_NAME + ) -> PyProjectData: return cls( path=path, tool_name=tool_name, @@ -125,8 +131,8 @@ def has_build_package( 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", _given_result: _t.GivenPyProjectResult = None, ) -> PyProjectData: From 10a96fbae766259cb4fdd5b80efe7f1c5a9a07a4 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 21:40:52 +0200 Subject: [PATCH 124/162] introduce DI for the inference config for theintegration points --- src/setuptools_scm/_integration/setuptools.py | 6 +++-- src/setuptools_scm/_types.py | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 5283c0d5..0c4fd9d2 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -70,6 +70,7 @@ def version_keyword( value: bool | dict[str, Any] | Callable[[], dict[str, Any]], *, _given_pyproject_data: _t.GivenPyProjectResult = 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 @@ -98,7 +99,7 @@ def version_keyword( log.debug("Configuration issue in pyproject.toml: %s", e) return - result = get_version_inference_config( + result = _get_version_inference_config( dist_name=dist_name, current_version=dist.metadata.version, pyproject_data=pyproject_data, @@ -113,6 +114,7 @@ def infer_version( dist: setuptools.Distribution, *, _given_pyproject_data: _t.GivenPyProjectResult = 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 @@ -134,7 +136,7 @@ def infer_version( log.debug("Configuration issue in pyproject.toml: %s", e) return - result = get_version_inference_config( + result = _get_version_inference_config( dist_name=dist_name, current_version=dist.metadata.version, pyproject_data=pyproject_data, diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index 703636f5..4a3bee99 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -5,6 +5,7 @@ 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 @@ -36,3 +37,24 @@ 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: object) -> 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, + was_set_by_infer: bool = False, + ) -> VersionInferenceApplicable: # pragma: no cover - structural type + ... From 2bfc5685939957808dd99d2c6c02fc29a16c893e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 22:01:17 +0200 Subject: [PATCH 125/162] read version from setup.cfg --- src/setuptools_scm/_integration/setup_cfg.py | 32 +++++++--- src/setuptools_scm/_integration/setuptools.py | 12 ++-- testing/test_integration.py | 58 ++++++++++++++++++- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/src/setuptools_scm/_integration/setup_cfg.py index e904d7d1..c03e521c 100644 --- a/src/setuptools_scm/_integration/setup_cfg.py +++ b/src/setuptools_scm/_integration/setup_cfg.py @@ -2,20 +2,36 @@ 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) -> SetuptoolsBasicData: + base = 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 0c4fd9d2..9af92c52 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -12,7 +12,7 @@ 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 extract_from_legacy from .toml import InvalidTomlError from .version_inference import get_version_inference_config @@ -85,7 +85,8 @@ 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) + dist_name: str | None = legacy_data.name was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) @@ -101,7 +102,7 @@ def version_keyword( result = _get_version_inference_config( dist_name=dist_name, - current_version=dist.metadata.version, + current_version=legacy_data.version or pyproject_data.project.get("version"), pyproject_data=pyproject_data, overrides=overrides, was_set_by_infer=was_set_by_infer, @@ -125,7 +126,8 @@ def infer_version( _log_hookstart("infer_version", dist) - dist_name = _dist_name_from_legacy(dist) + legacy_data = extract_from_legacy(dist) + dist_name = legacy_data.name try: pyproject_data = read_pyproject(_given_result=_given_pyproject_data) @@ -138,7 +140,7 @@ def infer_version( result = _get_version_inference_config( dist_name=dist_name, - current_version=dist.metadata.version, + current_version=legacy_data.version or pyproject_data.project.get("version"), pyproject_data=pyproject_data, ) result.apply(dist) diff --git a/testing/test_integration.py b/testing/test_integration.py index 12882059..152eb2f5 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -18,6 +18,7 @@ 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: @@ -645,11 +646,64 @@ 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 with version in setup.cfg + cfg = tmp_path / "setup.cfg" + cfg.write_text( + textwrap.dedent( + """ + [metadata] + name = legacy-proj + version = 0.9.0 + """ + ), + encoding="utf-8", + ) + + # No pyproject.toml + monkeypatch.chdir(tmp_path) + + dist = create_clean_distribution("legacy-proj") + + # Using keyword should detect an existing version via setup.cfg and avoid inferring + from setuptools_scm._integration import setuptools as setuptools_integration + from setuptools_scm._integration.pyproject_reading import PyProjectData + + setuptools_integration.version_keyword( + dist, + "use_scm_version", + True, + _given_pyproject_data=PyProjectData.empty(tmp_path / "pyproject.toml"), + ) + + # 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, From 1bea2c9842e80096c4f152a2e99280473e63fffc Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 22:13:44 +0200 Subject: [PATCH 126/162] undo support for simplified enabling --- .../_integration/pyproject_reading.py | 35 +++---------------- src/setuptools_scm/_integration/setuptools.py | 1 + .../_integration/version_inference.py | 14 +++----- testing/test_integration.py | 33 ++++++++++------- testing/test_version_inference.py | 11 +++--- 5 files changed, 37 insertions(+), 57 deletions(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 6b5f4531..fb6151e5 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -85,39 +85,14 @@ def should_infer(self) -> bool: """ Determine if setuptools_scm should infer version based on configuration. - This method only considers the pyproject.toml configuration state. - It does not consider version_keyword context (overrides always infer). + Only infer when an explicit [tool.setuptools_scm] section is present. + The presence of setuptools-scm in build-system.requires or + project.dynamic does NOT auto-enable inference. Returns: - True if version inference should proceed based on configuration - - Raises: - ValueError: If setuptools-scm is required but dynamic=['version'] is missing + True if [tool.setuptools_scm] is present, otherwise False """ - # If there's a tool section, always infer - if self.section_present: - return True - - # If not required, don't auto-activate for infer_version - if not self.is_required: - return False - - # setuptools-scm is required but no tool section - if not self.project_present: - # No project section - don't auto-activate - return False - - # Project section exists - check for dynamic=['version'] - 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." - ) - - # All conditions met - return True + return self.section_present def has_build_package( diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 9af92c52..ef579366 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -138,6 +138,7 @@ def infer_version( log.debug("Configuration issue in pyproject.toml: %s", e) return + # Only infer when tool section present per get_version_inference_config result = _get_version_inference_config( dist_name=dist_name, current_version=legacy_data.version or pyproject_data.project.get("version"), diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index 8bd12d6b..8e4d321f 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -149,14 +149,10 @@ def get_version_inference_config( dist_name=dist_name, pyproject_data=pyproject_data, overrides=overrides ) - # infer_version path: decide based on pyproject configuration only - try: - if pyproject_data.should_infer(): - return VersionInferenceConfig( - dist_name=dist_name, pyproject_data=pyproject_data, overrides=None - ) - except ValueError: - # Auto-activation should not error in infer_version context → skip silently - return VersionInferenceNoOp() + # infer_version path: only infer when [tool.setuptools_scm] section is present + if pyproject_data.should_infer(): + return VersionInferenceConfig( + dist_name=dist_name, pyproject_data=pyproject_data, overrides=None + ) return VersionInferenceNoOp() diff --git a/testing/test_integration.py b/testing/test_integration.py index 152eb2f5..888eb3de 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -739,7 +739,7 @@ def test_git_archival_plugin_ignored(tmp_path: Path, ep_name: str) -> None: def test_pyproject_build_system_requires_setuptools_scm(wd: WorkDir) -> None: - """Test that setuptools_scm is enabled when present in build-system.requires""" + """With only build-system.requires and dynamic version, no auto-enable without tool section.""" if sys.version_info < (3, 11): pytest.importorskip("tomli") @@ -761,13 +761,13 @@ def test_pyproject_build_system_requires_setuptools_scm(wd: WorkDir) -> None: wd.write("setup.py", "__import__('setuptools').setup()") res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("0.1.dev0+d20090213") + assert res == "0.0.0" 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""" + """Dash variant also does not auto-enable without tool section.""" if sys.version_info < (3, 11): pytest.importorskip("tomli") @@ -789,9 +789,12 @@ def test_pyproject_build_system_requires_setuptools_scm_dash_variant( wd.write("setup.py", "__import__('setuptools').setup()") res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("0.1.dev0+d20090213") + assert res == "0.0.0" +@pytest.mark.xfail( + reason="we currently dont support dynamic version without tool section" +) 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): @@ -847,7 +850,7 @@ def test_pyproject_build_system_requires_not_present(wd: WorkDir) -> None: 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""" + """Tool section controls enablement; build-system.requires may coexist.""" if sys.version_info < (3, 11): pytest.importorskip("tomli") @@ -887,7 +890,7 @@ def test_extract_package_name(base_name: str, requirements: str) -> None: def test_build_requires_integration_with_config_reading(wd: WorkDir) -> None: - """Test that Configuration.from_file handles build-system.requires automatically""" + """Configuration.from_file still accepts build-system.requires without tool section.""" if sys.version_info < (3, 11): pytest.importorskip("tomli") @@ -1092,6 +1095,9 @@ def test_integration_function_call_order( ) +@pytest.mark.xfail( + reason="we currently dont support dynamic version without tool section" +) def test_infer_version_with_build_requires_no_tool_section( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1119,14 +1125,14 @@ def test_infer_version_with_build_requires_no_tool_section( # Call infer_version with direct data injection - no file I/O! infer_version(dist, _given_pyproject_data=pyproject_data) - # 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 +@pytest.mark.xfail( + reason="we currently dont support dynamic version without tool section" +) def test_infer_version_with_build_requires_dash_variant_no_tool_section( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1154,11 +1160,8 @@ def test_infer_version_with_build_requires_dash_variant_no_tool_section( # Call infer_version with direct data injection - no file I/O! infer_version(dist, _given_pyproject_data=pyproject_data) - # 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 @@ -1221,6 +1224,9 @@ def test_version_keyword_no_scm_dependency_works( assert dist.metadata.version == "1.0.0" +@pytest.mark.xfail( + reason="we currently dont support dynamic version without tool section" +) def test_verify_dynamic_version_when_required_missing_dynamic() -> None: """Test that should_infer raises ValueError when setuptools-scm is in build-system.requires but dynamic=['version'] is missing""" from setuptools_scm._integration.pyproject_reading import PyProjectData @@ -1261,6 +1267,9 @@ def test_verify_dynamic_version_when_required_with_tool_section() -> None: assert pyproject_data.should_infer() is True +@pytest.mark.xfail( + reason="we currently dont support dynamic version without tool section" +) def test_verify_dynamic_version_when_required_with_dynamic() -> None: """Test that verification passes when setuptools-scm is in build-system.requires and dynamic=['version'] is set""" from setuptools_scm._integration.pyproject_reading import PyProjectData diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index e1296aac..5d649833 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -89,7 +89,7 @@ def test_no_setuptools_scm_config_infer_version(self) -> None: 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.""" + """We infer when setuptools-scm is not configured but use_scm_version=True.""" result = get_version_inference_config( dist_name="test_package", current_version=None, @@ -102,7 +102,7 @@ def test_no_setuptools_scm_config_version_keyword(self) -> None: assert result.overrides == {} 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.""" + """We don't infer without tool section even if required: infer_version path.""" result = get_version_inference_config( dist_name="test_package", current_version=None, @@ -142,18 +142,17 @@ def test_setuptools_scm_required_no_project_section_version_keyword_with_config( 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.""" + """We only infer when tool section present, regardless of required/project presence.""" result = get_version_inference_config( dist_name="test_package", current_version=None, pyproject_data=PyProjectData.for_testing(True, False, True), ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" + assert isinstance(result, VersionInferenceNoOp) def test_tool_section_present(self) -> None: - """Test that we infer when tool section is present.""" + """We infer when tool section is present.""" result = get_version_inference_config( dist_name="test_package", current_version=None, From 9dff8f23a06ff6e82a547f130cdbeaa71b721ef0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Aug 2025 22:17:40 +0200 Subject: [PATCH 127/162] update docs --- docs/config.md | 9 +++++---- docs/usage.md | 53 ++++++++++++++++++++------------------------------ 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/docs/config.md b/docs/config.md index 80ad24fd..c89a2d24 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2,11 +2,12 @@ ## When is configuration needed? -Starting with setuptools-scm 8.1+, explicit configuration is **optional** in many cases: +Starting with setuptools-scm 9.0, explicit configuration is required to enable +version inference, either via the `[tool.setuptools_scm]` section or the +`use_scm_version` setup keyword. Listing `setuptools_scm` in `build-system.requires` +and declaring `project.dynamic = ["version"]` no longer auto-enables inference. -- **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: +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/usage.md b/docs/usage.md index 28e0bc52..a3826727 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -7,30 +7,11 @@ 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 two ways to enable `setuptools-scm` at build time: -### Automatic Configuration (Recommended for Simple Cases) +### Explicit Configuration (required) -For projects that don't need custom configuration, simply include `setuptools-scm` -in your build requirements: - -```toml title="pyproject.toml" -[build-system] -requires = ["setuptools>=80", "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: +Add a `tool.setuptools_scm` section to explicitly opt-in to version inference: ```toml title="pyproject.toml" [build-system] @@ -51,20 +32,28 @@ 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 + +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. -!!! info "How Automatic Detection Works" +!!! warning "Simplified activation removed" - When setuptools-scm is listed in `build-system.requires`, it automatically detects this during the build process and activates with default settings. This means: + Previous documentation described a "simplified" activation where listing + `setuptools_scm` in `build-system.requires` together with `project.dynamic = ["version"]` + would auto-enable version inference. This behavior has been removed due to + regressions and ambiguous activation. You must explicitly opt in via either: - - ✅ **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 + - a `[tool.setuptools_scm]` section, or + - the `use_scm_version` setup keyword. - Both package names are detected: `setuptools_scm` and `setuptools-scm` (with dash). + The presence of `setuptools_scm` (or `setuptools-scm`) in `build-system.requires` + is still recommended to ensure the dependency is available during builds, but it + no longer enables version inference by itself. ### Version files From 637dd75f3e79098f5268d59f2babd8c40484790e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Aug 2025 07:50:45 +0200 Subject: [PATCH 128/162] onboard serena --- .serena/memories/done_checklist.md | 16 ++++++ .serena/memories/project_overview.md | 28 ++++++++++ .serena/memories/style_and_conventions.md | 17 ++++++ .serena/memories/suggested_commands.md | 30 ++++++++++ .serena/project.yml | 68 +++++++++++++++++++++++ 5 files changed, 159 insertions(+) create mode 100644 .serena/memories/done_checklist.md create mode 100644 .serena/memories/project_overview.md create mode 100644 .serena/memories/style_and_conventions.md create mode 100644 .serena/memories/suggested_commands.md create mode 100644 .serena/project.yml 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" From 5e8feec387e7cc76275ba9e0ac3e7a63e51276f8 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Aug 2025 07:59:29 +0200 Subject: [PATCH 129/162] Add project_version property to PyProjectData to enhance readability --- src/setuptools_scm/_integration/pyproject_reading.py | 9 +++++++++ src/setuptools_scm/_integration/setuptools.py | 7 +++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index fb6151e5..a501a6b8 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -81,6 +81,15 @@ def empty( def project_name(self) -> str | None: return self.project.get("name") + @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. diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index ef579366..293fb4e6 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -3,7 +3,6 @@ import logging import warnings -from pathlib import Path from typing import Any from typing import Callable @@ -95,14 +94,14 @@ def version_keyword( 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(Path("pyproject.toml"), "setuptools_scm") + pyproject_data = PyProjectData.empty() except InvalidTomlError as e: log.debug("Configuration issue in pyproject.toml: %s", e) return result = _get_version_inference_config( dist_name=dist_name, - current_version=legacy_data.version or pyproject_data.project.get("version"), + current_version=legacy_data.version or pyproject_data.project_version, pyproject_data=pyproject_data, overrides=overrides, was_set_by_infer=was_set_by_infer, @@ -141,7 +140,7 @@ def infer_version( # Only infer when tool section present per get_version_inference_config result = _get_version_inference_config( dist_name=dist_name, - current_version=legacy_data.version or pyproject_data.project.get("version"), + current_version=legacy_data.version or pyproject_data.project_version, pyproject_data=pyproject_data, ) result.apply(dist) From de701c0145ca2c4b642b0332739e9186cbdef2b5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Aug 2025 09:52:08 +0200 Subject: [PATCH 130/162] revise version inference logic - drop magic boolean parameters - drop unused exception creator - simplify calls --- src/setuptools_scm/_integration/setuptools.py | 14 ++++- .../_integration/version_inference.py | 54 +++++-------------- src/setuptools_scm/_types.py | 1 - testing/test_version_inference.py | 49 +++++++---------- 4 files changed, 43 insertions(+), 75 deletions(-) diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 293fb4e6..36f26a16 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -89,6 +89,10 @@ def version_keyword( was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) + # 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(_given_result=_given_pyproject_data) @@ -99,12 +103,18 @@ def version_keyword( log.debug("Configuration issue in pyproject.toml: %s", e) return + # 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=legacy_data.version or pyproject_data.project_version, + current_version=current_version, pyproject_data=pyproject_data, overrides=overrides, - was_set_by_infer=was_set_by_infer, ) result.apply(dist) diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index 8e4d321f..106f30eb 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -27,13 +27,6 @@ def apply(self, dist: Any) -> None: 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, @@ -45,7 +38,6 @@ def apply(self, dist: Any) -> None: if maybe_version is None: _version_missing(config) else: - assert dist.metadata.version is None dist.metadata.version = maybe_version # Mark that this version was set by infer_version if overrides is None (infer_version context) @@ -68,17 +60,6 @@ def apply(self, dist: Any) -> None: 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 - - class VersionInferenceNoOp: """No operation result - silent skip.""" @@ -89,7 +70,6 @@ def apply(self, dist: Any) -> None: VersionInferenceResult = Union[ VersionInferenceConfig, # Proceed with inference VersionInferenceError, # Show error/warning - VersionInferenceException, # Raise exception VersionInferenceNoOp, # Don't infer (silent) ] @@ -99,7 +79,6 @@ def get_version_inference_config( 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. @@ -109,7 +88,6 @@ 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 @@ -118,41 +96,33 @@ def get_version_inference_config( if dist_name is None: dist_name = pyproject_data.project_name - # If a version is already present, decide based on context (infer_version vs version_keyword) + # Never infer a version for setuptools-scm itself + if dist_name == "setuptools-scm": + return VersionInferenceNoOp() + + # If a version already exists, short-circuit by context if current_version is not None: - # infer_version call (overrides is None) should be a no-op if version already exists if overrides is None: + # infer_version called and a version is already present → do nothing return VersionInferenceNoOp() - - if not was_set_by_infer: + else: + # version_keyword context - always warn if version already set return VersionInferenceError( f"version of {dist_name} already set", should_warn=pyproject_data.should_infer(), ) - # Version was set by infer_version previously - if overrides: - # Non-empty overrides from version_keyword → re-infer with overrides - return VersionInferenceConfig( - dist_name=dist_name, pyproject_data=pyproject_data, overrides=overrides - ) - # Empty overrides dict from version_keyword → keep existing version - return VersionInferenceNoOp() - - # Do not infer a version for setuptools-scm itself - if dist_name == "setuptools-scm": - return VersionInferenceNoOp() - - # version_keyword path: any overrides (empty or not) mean we should infer + # No version present yet if overrides is not None: + # version_keyword path: any overrides (empty or not) mean we should infer return VersionInferenceConfig( dist_name=dist_name, pyproject_data=pyproject_data, overrides=overrides ) - # infer_version path: only infer when [tool.setuptools_scm] section is present + # infer_version path: only infer when [tool.setuptools_scm] is present if pyproject_data.should_infer(): return VersionInferenceConfig( - dist_name=dist_name, pyproject_data=pyproject_data, overrides=None + dist_name=dist_name, pyproject_data=pyproject_data, overrides=overrides ) return VersionInferenceNoOp() diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index 4a3bee99..cf34da13 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -55,6 +55,5 @@ def __call__( current_version: str | None, pyproject_data: PyProjectData, overrides: dict[str, object] | None = None, - was_set_by_infer: bool = False, ) -> VersionInferenceApplicable: # pragma: no cover - structural type ... diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index 5d649833..1cdf81c0 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -3,7 +3,6 @@ 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 get_version_inference_config @@ -11,43 +10,40 @@ 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.""" + def test_version_already_set_with_overrides(self) -> None: + """Test that we get an error when version is already set and 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"} + assert isinstance(result, VersionInferenceError) + assert "version of test_package already set" in result.message - def test_version_already_set_by_infer_no_overrides(self) -> None: + def test_version_already_set_no_overrides(self) -> None: """infer_version call with existing version should be a no-op.""" 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, VersionInferenceNoOp) - 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).""" + def test_version_already_set_empty_overrides(self) -> None: + """Test that we get an error when version is already set 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) + assert isinstance(result, VersionInferenceError) + assert "version of test_package already set" in result.message def test_version_already_set_by_something_else(self) -> None: """infer_version call with existing version set by something else should be a no-op.""" @@ -56,7 +52,6 @@ def test_version_already_set_by_something_else(self) -> None: current_version="1.0.0", pyproject_data=PyProjectData.for_testing(True, True, True), overrides=None, - was_set_by_infer=False, ) assert isinstance(result, VersionInferenceNoOp) @@ -83,7 +78,8 @@ def test_no_setuptools_scm_config_infer_version(self) -> None: dist_name="test_package", current_version=None, pyproject_data=PyProjectData.for_testing(False, False, True), - overrides=None, # infer_version call + overrides=None, + # infer_version call ) assert isinstance(result, VersionInferenceNoOp) @@ -94,7 +90,8 @@ def test_no_setuptools_scm_config_version_keyword(self) -> None: dist_name="test_package", current_version=None, pyproject_data=PyProjectData.for_testing(False, False, True), - overrides={}, # version_keyword call with use_scm_version=True + overrides={}, + # version_keyword call with use_scm_version=True ) assert isinstance(result, VersionInferenceConfig) @@ -107,7 +104,8 @@ def test_setuptools_scm_required_no_project_section_infer_version(self) -> None: dist_name="test_package", current_version=None, pyproject_data=PyProjectData.for_testing(True, False, False), - overrides=None, # infer_version call + overrides=None, + # infer_version call ) assert isinstance(result, VersionInferenceNoOp) @@ -118,7 +116,8 @@ def test_setuptools_scm_required_no_project_section_version_keyword(self) -> Non dist_name="test_package", current_version=None, pyproject_data=PyProjectData.for_testing(True, False, False), - overrides={}, # version_keyword call with use_scm_version=True + overrides={}, + # version_keyword call with use_scm_version=True ) assert isinstance(result, VersionInferenceConfig) @@ -134,7 +133,8 @@ def test_setuptools_scm_required_no_project_section_version_keyword_with_config( dist_name="test_package", current_version=None, pyproject_data=PyProjectData.for_testing(True, False, False), - overrides=overrides, # version_keyword call with use_scm_version={config} + overrides=overrides, + # version_keyword call with use_scm_version={config} ) assert isinstance(result, VersionInferenceConfig) @@ -191,7 +191,6 @@ def test_version_already_set_none_dist_name(self) -> None: current_version="1.0.0", pyproject_data=PyProjectData.for_testing(True, True, True), overrides=None, - was_set_by_infer=False, ) assert isinstance(result, VersionInferenceNoOp) @@ -260,13 +259,3 @@ 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 From 8669af074256b14e12a0bf17cc0ef937edfaa79e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Aug 2025 11:04:15 +0200 Subject: [PATCH 131/162] chore: add serena cache to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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/ From 9293e512129528675edcaa414b6576eb9235b0a8 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Aug 2025 11:05:15 +0200 Subject: [PATCH 132/162] reiterate version inference logic again the decission tree is massively simplified --- .../_integration/pyproject_reading.py | 1 + src/setuptools_scm/_integration/setup_cfg.py | 8 +- src/setuptools_scm/_integration/setuptools.py | 7 +- .../_integration/version_inference.py | 60 ++-- src/setuptools_scm/_types.py | 4 +- testing/test_integration.py | 42 +-- testing/test_version_inference.py | 286 +++++++----------- 7 files changed, 170 insertions(+), 238 deletions(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index a501a6b8..f7636d80 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -35,6 +35,7 @@ class PyProjectData: @classmethod def for_testing( cls, + *, is_required: bool = False, section_present: bool = False, project_present: bool = False, diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/src/setuptools_scm/_integration/setup_cfg.py index c03e521c..4e485600 100644 --- a/src/setuptools_scm/_integration/setup_cfg.py +++ b/src/setuptools_scm/_integration/setup_cfg.py @@ -28,8 +28,12 @@ def read_setup_cfg(input: str | os.PathLike[str] = "setup.cfg") -> SetuptoolsBas return SetuptoolsBasicData(path=path, name=name, version=version) -def extract_from_legacy(dist: setuptools.Distribution) -> SetuptoolsBasicData: - base = read_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: diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 36f26a16..aa1c645a 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -11,6 +11,7 @@ from .. import _types as _t from .pyproject_reading import PyProjectData from .pyproject_reading import read_pyproject +from .setup_cfg import SetuptoolsBasicData from .setup_cfg import extract_from_legacy from .toml import InvalidTomlError from .version_inference import get_version_inference_config @@ -69,6 +70,7 @@ def version_keyword( 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 @@ -84,7 +86,7 @@ def version_keyword( "dist_name may not be specified in the setup keyword " ) - legacy_data = extract_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) @@ -124,6 +126,7 @@ 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 @@ -135,7 +138,7 @@ def infer_version( _log_hookstart("infer_version", dist) - legacy_data = extract_from_legacy(dist) + legacy_data = extract_from_legacy(dist, _given_legacy_data=_given_legacy_data) dist_name = legacy_data.name try: diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index 106f30eb..cc2dea01 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,7 +23,7 @@ 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 @@ -42,34 +44,32 @@ def apply(self, dist: Any) -> None: # 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) + warnings.warn(self.message) 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 + VersionInferenceWarning, # Show warning VersionInferenceNoOp, # Don't infer (silent) ] @@ -92,37 +92,21 @@ def get_version_inference_config( Returns: VersionInferenceResult with the decision and configuration """ - # Normalize name from project metadata when not provided - if dist_name is None: - dist_name = pyproject_data.project_name - # Never infer a version for setuptools-scm itself - if dist_name == "setuptools-scm": - return VersionInferenceNoOp() + config = VersionInferenceConfig( + dist_name=dist_name, + pyproject_data=pyproject_data, + overrides=overrides, + ) + + inference_implied = pyproject_data.should_infer() or overrides is not None - # If a version already exists, short-circuit by context - if current_version is not None: - if overrides is None: - # infer_version called and a version is already present → do nothing - return VersionInferenceNoOp() + if inference_implied: + if current_version is None: + return config else: - # version_keyword context - always warn if version already set - return VersionInferenceError( + return VersionInferenceWarning( f"version of {dist_name} already set", - should_warn=pyproject_data.should_infer(), ) - - # No version present yet - if overrides is not None: - # version_keyword path: any overrides (empty or not) mean we should infer - return VersionInferenceConfig( - dist_name=dist_name, pyproject_data=pyproject_data, overrides=overrides - ) - - # infer_version path: only infer when [tool.setuptools_scm] is present - if pyproject_data.should_infer(): - return VersionInferenceConfig( - dist_name=dist_name, pyproject_data=pyproject_data, overrides=overrides - ) - - return VersionInferenceNoOp() + else: + return VersionInferenceNoOp() diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index cf34da13..4f8874fb 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -10,6 +10,8 @@ from typing import Tuple from typing import Union +from setuptools import Distribution + if TYPE_CHECKING: import sys @@ -42,7 +44,7 @@ class VersionInferenceApplicable(Protocol): """A result object from version inference decision that can be applied to a dist.""" - def apply(self, dist: object) -> None: # pragma: no cover - structural type + def apply(self, dist: Distribution) -> None: # pragma: no cover - structural type ... diff --git a/testing/test_integration.py b/testing/test_integration.py index 888eb3de..c9092738 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -672,35 +672,37 @@ def test_unicode_in_setup_cfg(tmp_path: Path) -> None: def test_setup_cfg_version_prevents_inference_version_keyword( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - # Legacy project with version in setup.cfg - cfg = tmp_path / "setup.cfg" - cfg.write_text( - textwrap.dedent( - """ - [metadata] - name = legacy-proj - version = 0.9.0 - """ - ), - encoding="utf-8", - ) - - # No pyproject.toml + # 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 setup.cfg and avoid inferring + # 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 - setuptools_integration.version_keyword( - dist, - "use_scm_version", - True, - _given_pyproject_data=PyProjectData.empty(tmp_path / "pyproject.toml"), + # 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 diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index 1cdf81c0..2ce12d0c 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -1,261 +1,197 @@ from __future__ import annotations +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 VersionInferenceNoOp +from setuptools_scm._integration.version_inference import VersionInferenceWarning from setuptools_scm._integration.version_inference import get_version_inference_config +# Common test data +DEFAULT_PYPROJECT_DATA = PyProjectData.for_testing( + is_required=True, section_present=True, project_present=True +) + + +def expect_config( + *, + dist_name: str | None = "test_package", + current_version: str | None, + pyproject_data: PyProjectData = DEFAULT_PYPROJECT_DATA, + overrides: dict[str, Any] | None = None, + expected_type: type = VersionInferenceConfig, + expected_message: str | None = None, +) -> 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, + ) + + if not isinstance(result, expected_type): + pytest.fail(f"{type(result).__name__} != {expected_type.__name__}") + + if expected_type == VersionInferenceWarning and expected_message: + assert isinstance(result, VersionInferenceWarning) + assert expected_message in result.message + elif expected_type == VersionInferenceConfig: + assert isinstance(result, VersionInferenceConfig) + assert result.dist_name == dist_name + assert result.overrides == overrides + class TestVersionInferenceDecision: """Test the version inference decision logic.""" - def test_version_already_set_with_overrides(self) -> None: - """Test that we get an error when version is already set and overrides provided.""" - result = get_version_inference_config( - dist_name="test_package", - current_version="1.0.0", - pyproject_data=PyProjectData.for_testing(True, True, True), + def test_missing_version_with_overrides_triggers(self) -> None: + """Test that version_keyword context with overrides infers when no existing version.""" + expect_config( + current_version=None, # version_keyword passes None when version was set by infer overrides={"key": "value"}, ) - assert isinstance(result, VersionInferenceError) - assert "version of test_package already set" in result.message + def test_overrides_on_existing_version_warns(self) -> None: + """note: version_keyword opts out of inference if + version is set by something else or overrides are empty""" + expect_config( + current_version="1.0.0", # version set by something else (setup.cfg, etc.) + overrides={"key": "value"}, + expected_type=VersionInferenceWarning, + expected_message="version of test_package already set", + ) def test_version_already_set_no_overrides(self) -> None: - """infer_version call with existing version should be a no-op.""" - result = get_version_inference_config( - dist_name="test_package", + """infer_version call with existing version warns when inference is implied.""" + expect_config( current_version="1.0.0", - pyproject_data=PyProjectData.for_testing(True, True, True), overrides=None, + expected_type=VersionInferenceWarning, + expected_message="version of test_package already set", ) - assert isinstance(result, VersionInferenceNoOp) - - def test_version_already_set_empty_overrides(self) -> None: - """Test that we get an error when version is already set 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), + def test_version_keyword_with_empty_overrides(self) -> None: + """Test that version_keyword context with empty overrides infers when no existing version.""" + expect_config( + current_version=None, # version_keyword handles early exit, so this is what we see overrides={}, ) - assert isinstance(result, VersionInferenceError) - assert "version of test_package already set" in result.message + def test_version_keyword_empty_overrides_existing_version(self) -> None: + """Test that version_keyword context with empty overrides and existing version errors.""" + expect_config( + current_version="1.0.0", # version set by something else (setup.cfg, etc.) + overrides={}, + expected_type=VersionInferenceWarning, + expected_message="version of test_package already set", + ) def test_version_already_set_by_something_else(self) -> None: - """infer_version call with existing version set by something else should be a no-op.""" - result = get_version_inference_config( - dist_name="test_package", + """infer_version call with existing version warns when inference is implied.""" + expect_config( current_version="1.0.0", - pyproject_data=PyProjectData.for_testing(True, True, True), overrides=None, + expected_type=VersionInferenceWarning, + expected_message="version of test_package already set", ) - assert isinstance(result, VersionInferenceNoOp) - - 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", + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(False, False, True), + pyproject_data=PyProjectData.for_testing( + is_required=False, section_present=False, project_present=True + ), overrides=None, - # infer_version call + expected_type=VersionInferenceNoOp, ) - assert isinstance(result, VersionInferenceNoOp) - def test_no_setuptools_scm_config_version_keyword(self) -> None: """We infer when setuptools-scm is not configured but use_scm_version=True.""" - result = get_version_inference_config( - dist_name="test_package", + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(False, False, True), + pyproject_data=PyProjectData.for_testing( + is_required=False, section_present=False, project_present=True + ), overrides={}, - # version_keyword call with use_scm_version=True ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - assert result.overrides == {} - def test_setuptools_scm_required_no_project_section_infer_version(self) -> None: """We don't infer without tool section even if required: infer_version path.""" - result = get_version_inference_config( - dist_name="test_package", + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, False, False), + pyproject_data=PyProjectData.for_testing( + is_required=True, section_present=False, project_present=False + ), overrides=None, - # infer_version call + expected_type=VersionInferenceNoOp, ) - 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), + pyproject_data=PyProjectData.for_testing( + is_required=True, section_present=False, project_present=False + ), overrides={}, - # version_keyword call with use_scm_version=True ) - 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=PyProjectData.for_testing( + is_required=True, section_present=False, project_present=False + ), + overrides={"version_scheme": "calver"}, ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == "test_package" - assert result.overrides == overrides - def test_setuptools_scm_required_with_project_section(self) -> None: """We only infer when tool section present, regardless of required/project presence.""" - result = get_version_inference_config( - dist_name="test_package", + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, False, True), + pyproject_data=PyProjectData.for_testing( + is_required=True, section_present=False, project_present=True + ), + expected_type=VersionInferenceNoOp, ) - assert isinstance(result, VersionInferenceNoOp) - def test_tool_section_present(self) -> None: """We infer when tool section is present.""" - result = get_version_inference_config( - dist_name="test_package", + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(False, True, False), + pyproject_data=PyProjectData.for_testing( + is_required=False, section_present=True, project_present=False + ), ) - 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", + expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing(True, True, True), ) - 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( + expect_config( dist_name=None, current_version=None, - pyproject_data=PyProjectData.for_testing(True, True, True), ) - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name is None - def test_version_already_set_none_dist_name(self) -> None: - """infer_version call with None dist_name and existing version should be a no-op.""" - result = get_version_inference_config( + """infer_version call with None dist_name and existing version warns when inference is implied.""" + expect_config( dist_name=None, current_version="1.0.0", - pyproject_data=PyProjectData.for_testing(True, True, True), overrides=None, + expected_type=VersionInferenceWarning, + expected_message="version of None already set", ) - - assert isinstance(result, VersionInferenceNoOp) - - 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", - current_version=None, - pyproject_data=PyProjectData.for_testing(True, True, True), - overrides=overrides, - ) - - 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", - pyproject_data=pyproject_data, - overrides={"key": "value"}, - ) - - 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 From 9e91663c8d81a6ab54cdc9fffadfa525fb1e72d9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Aug 2025 11:34:17 +0200 Subject: [PATCH 133/162] introduce helpers for more compact version inference test writing --- .../_integration/version_inference.py | 1 + testing/test_version_inference.py | 121 ++++++++++-------- 2 files changed, 71 insertions(+), 51 deletions(-) diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index cc2dea01..cbfe9f61 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -60,6 +60,7 @@ def apply(self, dist: Distribution) -> None: warnings.warn(self.message) +@dataclass(frozen=True) class VersionInferenceNoOp: """No operation result - silent skip.""" diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index 2ce12d0c..bb3945c6 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -1,12 +1,12 @@ 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 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 @@ -15,6 +15,31 @@ is_required=True, section_present=True, project_present=True ) +PYPROJECT_WITHOUT_TOOL_SECTION = PyProjectData.for_testing( + is_required=True, section_present=False, project_present=True +) + +PYPROJECT_ONLY_REQUIRED = PyProjectData.for_testing( + is_required=True, section_present=False, project_present=False +) + +OVERRIDES = SimpleNamespace( + EMPTY={}, + CALVER={"version_scheme": "calver"}, + UNRELATED={"key": "value"}, + INFER_VERSION=None, +) + + +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( *, @@ -22,8 +47,9 @@ def expect_config( current_version: str | None, pyproject_data: PyProjectData = DEFAULT_PYPROJECT_DATA, overrides: dict[str, Any] | None = None, - expected_type: type = VersionInferenceConfig, - expected_message: str | None = None, + expected: type[VersionInferenceConfig] + | VersionInferenceWarning + | VersionInferenceNoOp, ) -> None: """Helper to test get_version_inference_config and assert expected result type.""" __tracebackhide__ = True @@ -34,16 +60,18 @@ def expect_config( overrides=overrides, ) - if not isinstance(result, expected_type): - pytest.fail(f"{type(result).__name__} != {expected_type.__name__}") + 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 - if expected_type == VersionInferenceWarning and expected_message: - assert isinstance(result, VersionInferenceWarning) - assert expected_message in result.message - elif expected_type == VersionInferenceConfig: - assert isinstance(result, VersionInferenceConfig) - assert result.dist_name == dist_name - assert result.overrides == overrides + assert result == expectation class TestVersionInferenceDecision: @@ -53,7 +81,8 @@ def test_missing_version_with_overrides_triggers(self) -> None: """Test that version_keyword context with overrides infers when no existing version.""" expect_config( current_version=None, # version_keyword passes None when version was set by infer - overrides={"key": "value"}, + overrides=OVERRIDES.UNRELATED, + expected=VersionInferenceConfig, ) def test_overrides_on_existing_version_warns(self) -> None: @@ -61,9 +90,8 @@ def test_overrides_on_existing_version_warns(self) -> None: version is set by something else or overrides are empty""" expect_config( current_version="1.0.0", # version set by something else (setup.cfg, etc.) - overrides={"key": "value"}, - expected_type=VersionInferenceWarning, - expected_message="version of test_package already set", + overrides=OVERRIDES.UNRELATED, + expected=WARNING_PACKAGE, ) def test_version_already_set_no_overrides(self) -> None: @@ -71,24 +99,23 @@ def test_version_already_set_no_overrides(self) -> None: expect_config( current_version="1.0.0", overrides=None, - expected_type=VersionInferenceWarning, - expected_message="version of test_package already set", + expected=WARNING_PACKAGE, ) def test_version_keyword_with_empty_overrides(self) -> None: """Test that version_keyword context with empty overrides infers when no existing version.""" expect_config( current_version=None, # version_keyword handles early exit, so this is what we see - overrides={}, + overrides=OVERRIDES.EMPTY, + expected=VersionInferenceConfig, ) def test_version_keyword_empty_overrides_existing_version(self) -> None: """Test that version_keyword context with empty overrides and existing version errors.""" expect_config( current_version="1.0.0", # version set by something else (setup.cfg, etc.) - overrides={}, - expected_type=VersionInferenceWarning, - expected_message="version of test_package already set", + overrides=OVERRIDES.EMPTY, + expected=WARNING_PACKAGE, ) def test_version_already_set_by_something_else(self) -> None: @@ -96,50 +123,43 @@ def test_version_already_set_by_something_else(self) -> None: expect_config( current_version="1.0.0", overrides=None, - expected_type=VersionInferenceWarning, - expected_message="version of test_package already set", + expected=WARNING_PACKAGE, ) 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.""" expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing( - is_required=False, section_present=False, project_present=True - ), + pyproject_data=PYPROJECT_WITHOUT_TOOL_SECTION, overrides=None, - expected_type=VersionInferenceNoOp, + expected=NOOP, ) def test_no_setuptools_scm_config_version_keyword(self) -> None: """We infer when setuptools-scm is not configured but use_scm_version=True.""" expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing( - is_required=False, section_present=False, project_present=True - ), - overrides={}, + pyproject_data=PYPROJECT_WITHOUT_TOOL_SECTION, + overrides=OVERRIDES.EMPTY, + expected=VersionInferenceConfig, ) def test_setuptools_scm_required_no_project_section_infer_version(self) -> None: """We don't infer without tool section even if required: infer_version path.""" expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing( - is_required=True, section_present=False, project_present=False - ), + pyproject_data=PYPROJECT_ONLY_REQUIRED, overrides=None, - expected_type=VersionInferenceNoOp, + expected=NOOP, ) 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.""" expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing( - is_required=True, section_present=False, project_present=False - ), - overrides={}, + pyproject_data=PYPROJECT_ONLY_REQUIRED, + overrides=OVERRIDES.EMPTY, + expected=VersionInferenceConfig, ) def test_setuptools_scm_required_no_project_section_version_keyword_with_config( @@ -148,20 +168,17 @@ def test_setuptools_scm_required_no_project_section_version_keyword_with_config( """Test that we DO infer when setuptools-scm is required but no project section and use_scm_version={config}.""" expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing( - is_required=True, section_present=False, project_present=False - ), - overrides={"version_scheme": "calver"}, + pyproject_data=PYPROJECT_ONLY_REQUIRED, + overrides=OVERRIDES.CALVER, + expected=VersionInferenceConfig, ) def test_setuptools_scm_required_with_project_section(self) -> None: """We only infer when tool section present, regardless of required/project presence.""" expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing( - is_required=True, section_present=False, project_present=True - ), - expected_type=VersionInferenceNoOp, + pyproject_data=PYPROJECT_WITHOUT_TOOL_SECTION, + expected=NOOP, ) def test_tool_section_present(self) -> None: @@ -171,12 +188,14 @@ def test_tool_section_present(self) -> None: pyproject_data=PyProjectData.for_testing( is_required=False, section_present=True, project_present=False ), + expected=VersionInferenceConfig, ) def test_both_required_and_tool_section(self) -> None: """Test that we infer when both required and tool section are present.""" expect_config( current_version=None, + expected=VersionInferenceConfig, ) def test_none_dist_name(self) -> None: @@ -184,6 +203,7 @@ def test_none_dist_name(self) -> None: expect_config( dist_name=None, current_version=None, + expected=VersionInferenceConfig, ) def test_version_already_set_none_dist_name(self) -> None: @@ -192,6 +212,5 @@ def test_version_already_set_none_dist_name(self) -> None: dist_name=None, current_version="1.0.0", overrides=None, - expected_type=VersionInferenceWarning, - expected_message="version of None already set", + expected=WARNING_NO_PACKAGE, ) From ab097e9c563fd27ff4bfd71c1889d963e2b2ab27 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Aug 2025 14:19:20 +0200 Subject: [PATCH 134/162] partial test creation - first iteration of more pinpointed version inference tests --- testing/test_version_inference.py | 223 ++++++++++++++++++++---------- 1 file changed, 150 insertions(+), 73 deletions(-) diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index bb3945c6..10a33d82 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -3,6 +3,8 @@ 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 VersionInferenceNoOp @@ -11,23 +13,26 @@ from setuptools_scm._integration.version_inference import get_version_inference_config # Common test data -DEFAULT_PYPROJECT_DATA = PyProjectData.for_testing( - is_required=True, section_present=True, project_present=True -) - -PYPROJECT_WITHOUT_TOOL_SECTION = PyProjectData.for_testing( - is_required=True, section_present=False, project_present=True -) - -PYPROJECT_ONLY_REQUIRED = PyProjectData.for_testing( - is_required=True, section_present=False, project_present=False +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"}, - INFER_VERSION=None, ) @@ -45,7 +50,7 @@ def expect_config( *, dist_name: str | None = "test_package", current_version: str | None, - pyproject_data: PyProjectData = DEFAULT_PYPROJECT_DATA, + pyproject_data: PyProjectData = PYPROJECT.DEFAULT, overrides: dict[str, Any] | None = None, expected: type[VersionInferenceConfig] | VersionInferenceWarning @@ -74,63 +79,144 @@ def expect_config( assert result == expectation -class TestVersionInferenceDecision: - """Test the version inference decision logic.""" +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", + ), + ], +) - def test_missing_version_with_overrides_triggers(self) -> None: - """Test that version_keyword context with overrides infers when no existing version.""" - expect_config( - current_version=None, # version_keyword passes None when version was set by infer - overrides=OVERRIDES.UNRELATED, - expected=VersionInferenceConfig, - ) - def test_overrides_on_existing_version_warns(self) -> None: - """note: version_keyword opts out of inference if - version is set by something else or overrides are empty""" - expect_config( - current_version="1.0.0", # version set by something else (setup.cfg, etc.) - overrides=OVERRIDES.UNRELATED, - expected=WARNING_PACKAGE, - ) +@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, + ) - def test_version_already_set_no_overrides(self) -> None: - """infer_version call with existing version warns when inference is implied.""" - expect_config( - current_version="1.0.0", - overrides=None, - expected=WARNING_PACKAGE, - ) - def test_version_keyword_with_empty_overrides(self) -> None: - """Test that version_keyword context with empty overrides infers when no existing version.""" - expect_config( - current_version=None, # version_keyword handles early exit, so this is what we see - overrides=OVERRIDES.EMPTY, - expected=VersionInferenceConfig, - ) +@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_version_keyword_empty_overrides_existing_version(self) -> None: - """Test that version_keyword context with empty overrides and existing version errors.""" - expect_config( - current_version="1.0.0", # version set by something else (setup.cfg, etc.) - overrides=OVERRIDES.EMPTY, - expected=WARNING_PACKAGE, - ) - def test_version_already_set_by_something_else(self) -> None: - """infer_version call with existing version warns when inference is implied.""" - expect_config( - current_version="1.0.0", - overrides=None, - expected=WARNING_PACKAGE, - ) +def test_no_config_no_infer() -> None: + expect_config( + current_version=None, + pyproject_data=PYPROJECT.WITHOUT_TOOL_SECTION, + overrides=OVERRIDES.NOT_GIVEN, + expected=NOOP, + ) + + +Expectation = SimpleNamespace + + +class TestVersionInferenceDecision: + """Test the version inference decision logic.""" + + @pytest.mark.parametrize( + "expectation", + [ + pytest.param( + Expectation( + current_version=None, + overrides=OVERRIDES.UNRELATED, + expected=VersionInferenceConfig, + ), + id="missing_version_with_overrides_triggers", + ), + pytest.param( + Expectation( + current_version="1.0.0", + overrides=OVERRIDES.UNRELATED, + expected=WARNING_PACKAGE, + ), + id="overrides_on_existing_version_warns", + ), + pytest.param( + Expectation( + current_version="1.0.0", + overrides=None, + expected=WARNING_PACKAGE, + ), + id="version_already_set_no_overrides", + ), + pytest.param( + Expectation( + current_version=None, + overrides=OVERRIDES.EMPTY, + expected=VersionInferenceConfig, + ), + id="version_keyword_with_empty_overrides", + ), + pytest.param( + Expectation( + current_version="1.0.0", + overrides=OVERRIDES.EMPTY, + expected=WARNING_PACKAGE, + ), + id="version_keyword_empty_overrides_existing_version", + ), + pytest.param( + Expectation( + current_version="1.0.0", + overrides=None, + expected=WARNING_PACKAGE, + ), + id="version_already_set_by_something_else", + ), + pytest.param( + Expectation( + current_version=None, + overrides=None, + expected=VersionInferenceConfig, + ), + id="both_required_and_tool_section", + ), + ], + ) + @pytest.mark.xfail(reason="TODO: fix this") + def test_default_package_scenarios(self, expectation: Expectation) -> None: + """Test version inference scenarios using default package name and pyproject data.""" + expectation.check() 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.""" expect_config( current_version=None, - pyproject_data=PYPROJECT_WITHOUT_TOOL_SECTION, + pyproject_data=PYPROJECT.WITHOUT_TOOL_SECTION, overrides=None, expected=NOOP, ) @@ -139,7 +225,7 @@ def test_no_setuptools_scm_config_version_keyword(self) -> None: """We infer when setuptools-scm is not configured but use_scm_version=True.""" expect_config( current_version=None, - pyproject_data=PYPROJECT_WITHOUT_TOOL_SECTION, + pyproject_data=PYPROJECT.WITHOUT_TOOL_SECTION, overrides=OVERRIDES.EMPTY, expected=VersionInferenceConfig, ) @@ -148,7 +234,7 @@ def test_setuptools_scm_required_no_project_section_infer_version(self) -> None: """We don't infer without tool section even if required: infer_version path.""" expect_config( current_version=None, - pyproject_data=PYPROJECT_ONLY_REQUIRED, + pyproject_data=PYPROJECT.ONLY_REQUIRED, overrides=None, expected=NOOP, ) @@ -157,7 +243,7 @@ def test_setuptools_scm_required_no_project_section_version_keyword(self) -> Non """Test that we DO infer when setuptools-scm is required but no project section and use_scm_version=True.""" expect_config( current_version=None, - pyproject_data=PYPROJECT_ONLY_REQUIRED, + pyproject_data=PYPROJECT.ONLY_REQUIRED, overrides=OVERRIDES.EMPTY, expected=VersionInferenceConfig, ) @@ -168,7 +254,7 @@ def test_setuptools_scm_required_no_project_section_version_keyword_with_config( """Test that we DO infer when setuptools-scm is required but no project section and use_scm_version={config}.""" expect_config( current_version=None, - pyproject_data=PYPROJECT_ONLY_REQUIRED, + pyproject_data=PYPROJECT.ONLY_REQUIRED, overrides=OVERRIDES.CALVER, expected=VersionInferenceConfig, ) @@ -177,7 +263,7 @@ def test_setuptools_scm_required_with_project_section(self) -> None: """We only infer when tool section present, regardless of required/project presence.""" expect_config( current_version=None, - pyproject_data=PYPROJECT_WITHOUT_TOOL_SECTION, + pyproject_data=PYPROJECT.WITHOUT_TOOL_SECTION, expected=NOOP, ) @@ -185,16 +271,7 @@ def test_tool_section_present(self) -> None: """We infer when tool section is present.""" expect_config( current_version=None, - pyproject_data=PyProjectData.for_testing( - is_required=False, section_present=True, project_present=False - ), - expected=VersionInferenceConfig, - ) - - def test_both_required_and_tool_section(self) -> None: - """Test that we infer when both required and tool section are present.""" - expect_config( - current_version=None, + pyproject_data=PYPROJECT.WITHOUT_PROJECT, expected=VersionInferenceConfig, ) From 3bfe5470d35d60eb6e920bf5b28a08c607816a2a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Aug 2025 16:15:10 +0200 Subject: [PATCH 135/162] drop useless test data for infer_version --- testing/test_version_inference.py | 112 ------------------------------ 1 file changed, 112 deletions(-) diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py index 10a33d82..24a1c82e 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -140,96 +140,9 @@ def test_no_config_no_infer() -> None: ) -Expectation = SimpleNamespace - - class TestVersionInferenceDecision: """Test the version inference decision logic.""" - @pytest.mark.parametrize( - "expectation", - [ - pytest.param( - Expectation( - current_version=None, - overrides=OVERRIDES.UNRELATED, - expected=VersionInferenceConfig, - ), - id="missing_version_with_overrides_triggers", - ), - pytest.param( - Expectation( - current_version="1.0.0", - overrides=OVERRIDES.UNRELATED, - expected=WARNING_PACKAGE, - ), - id="overrides_on_existing_version_warns", - ), - pytest.param( - Expectation( - current_version="1.0.0", - overrides=None, - expected=WARNING_PACKAGE, - ), - id="version_already_set_no_overrides", - ), - pytest.param( - Expectation( - current_version=None, - overrides=OVERRIDES.EMPTY, - expected=VersionInferenceConfig, - ), - id="version_keyword_with_empty_overrides", - ), - pytest.param( - Expectation( - current_version="1.0.0", - overrides=OVERRIDES.EMPTY, - expected=WARNING_PACKAGE, - ), - id="version_keyword_empty_overrides_existing_version", - ), - pytest.param( - Expectation( - current_version="1.0.0", - overrides=None, - expected=WARNING_PACKAGE, - ), - id="version_already_set_by_something_else", - ), - pytest.param( - Expectation( - current_version=None, - overrides=None, - expected=VersionInferenceConfig, - ), - id="both_required_and_tool_section", - ), - ], - ) - @pytest.mark.xfail(reason="TODO: fix this") - def test_default_package_scenarios(self, expectation: Expectation) -> None: - """Test version inference scenarios using default package name and pyproject data.""" - expectation.check() - - 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.""" - expect_config( - current_version=None, - pyproject_data=PYPROJECT.WITHOUT_TOOL_SECTION, - overrides=None, - expected=NOOP, - ) - - def test_no_setuptools_scm_config_version_keyword(self) -> None: - """We infer when setuptools-scm is not configured but use_scm_version=True.""" - expect_config( - current_version=None, - pyproject_data=PYPROJECT.WITHOUT_TOOL_SECTION, - overrides=OVERRIDES.EMPTY, - expected=VersionInferenceConfig, - ) - def test_setuptools_scm_required_no_project_section_infer_version(self) -> None: """We don't infer without tool section even if required: infer_version path.""" expect_config( @@ -259,14 +172,6 @@ def test_setuptools_scm_required_no_project_section_version_keyword_with_config( expected=VersionInferenceConfig, ) - def test_setuptools_scm_required_with_project_section(self) -> None: - """We only infer when tool section present, regardless of required/project presence.""" - expect_config( - current_version=None, - pyproject_data=PYPROJECT.WITHOUT_TOOL_SECTION, - expected=NOOP, - ) - def test_tool_section_present(self) -> None: """We infer when tool section is present.""" expect_config( @@ -274,20 +179,3 @@ def test_tool_section_present(self) -> None: pyproject_data=PYPROJECT.WITHOUT_PROJECT, expected=VersionInferenceConfig, ) - - def test_none_dist_name(self) -> None: - """Test that we handle None dist_name correctly.""" - expect_config( - dist_name=None, - current_version=None, - expected=VersionInferenceConfig, - ) - - def test_version_already_set_none_dist_name(self) -> None: - """infer_version call with None dist_name and existing version warns when inference is implied.""" - expect_config( - dist_name=None, - current_version="1.0.0", - overrides=None, - expected=WARNING_NO_PACKAGE, - ) From e78df43cb3b227add153772e0ba68325b181723c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 14 Aug 2025 11:44:24 +0200 Subject: [PATCH 136/162] remove integration tests that mirror version inference integration unittests --- testing/test_integration.py | 210 ------------------------------------ 1 file changed, 210 deletions(-) diff --git a/testing/test_integration.py b/testing/test_integration.py index c9092738..a7a4439d 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 @@ -72,200 +71,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") @@ -273,18 +78,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") @@ -305,7 +98,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.*"): @@ -325,7 +117,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}' @@ -402,7 +193,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}' From e85f66cfa61926d9809b4a539eca8903f4269540 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 14 Aug 2025 13:37:59 +0200 Subject: [PATCH 137/162] drop integration tests that mirror the version inference unittests --- testing/test_integration.py | 455 ------------------------------------ 1 file changed, 455 deletions(-) diff --git a/testing/test_integration.py b/testing/test_integration.py index a7a4439d..1020beb9 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -45,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() @@ -530,146 +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: - """With only build-system.requires and dynamic version, no auto-enable without tool section.""" - 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 == "0.0.0" - - -def test_pyproject_build_system_requires_setuptools_scm_dash_variant( - wd: WorkDir, -) -> None: - """Dash variant also does not auto-enable without tool section.""" - 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 == "0.0.0" - - -@pytest.mark.xfail( - reason="we currently dont support dynamic version without tool section" -) -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: - """Tool section controls enablement; build-system.requires may coexist.""" - 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", @@ -681,92 +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: - """Configuration.from_file still accepts build-system.requires without tool section.""" - 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_missing_section_no_longer_raises_error( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that missing [tool.setuptools_scm] section no longer raises error, creates valid config""" - 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 - - # This should no longer raise an error - instead it should create a valid configuration - # with default values and log a warning - config = Configuration.from_file( - name=wd.cwd.joinpath("pyproject.toml"), - dist_name="test-package", - ) - - # Should have created a valid configuration with default values - assert config.dist_name == "test-package" - assert config.version_scheme == "guess-next-dev" # default - assert config.local_scheme == "node-and-date" # default - - # 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. @@ -887,232 +659,6 @@ def test_integration_function_call_order( ) -@pytest.mark.xfail( - reason="we currently dont support dynamic version without tool section" -) -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""" - # Set up a git repository with a tag - wd.commit_testfile("test") - wd("git tag 1.0.0") - monkeypatch.chdir(wd.cwd) - - from setuptools_scm._integration.pyproject_reading import PyProjectData - from setuptools_scm._integration.setuptools import infer_version - - # Create pyproject data: setuptools_scm required, no tool section, project with dynamic=['version'] - pyproject_data = PyProjectData.for_testing( - is_required=True, - section_present=False, - project_present=True, - project_name="test-package-infer-version", - has_dynamic_version=True, - ) - - # Create clean distribution - dist = create_clean_distribution("test-package-infer-version") - - # Call infer_version with direct data injection - no file I/O! - infer_version(dist, _given_pyproject_data=pyproject_data) - - assert dist.metadata.version == "1.0.0" - - assert getattr(dist, "_setuptools_scm_version_set_by_infer", False) is True - - -@pytest.mark.xfail( - reason="we currently dont support dynamic version without tool section" -) -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""" - # Set up a git repository with a tag - wd.commit_testfile("test") - wd("git tag 1.0.0") - monkeypatch.chdir(wd.cwd) - - from setuptools_scm._integration.pyproject_reading import PyProjectData - from setuptools_scm._integration.setuptools import infer_version - - # Create pyproject data: setuptools-scm required, no tool section, project with dynamic=['version'] - pyproject_data = PyProjectData.for_testing( - is_required=True, - section_present=False, - project_present=True, - project_name="test-package-infer-version-dash", - has_dynamic_version=True, - ) - - # Create clean distribution - dist = create_clean_distribution("test-package-infer-version-dash") - - # Call infer_version with direct data injection - no file I/O! - infer_version(dist, _given_pyproject_data=pyproject_data) - - assert dist.metadata.version == "1.0.0" - - 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""" - # Set up a git repository with a tag - wd.commit_testfile("test") - wd("git tag 1.0.0") - monkeypatch.chdir(wd.cwd) - - from setuptools_scm._integration.pyproject_reading import PyProjectData - from setuptools_scm._integration.setuptools import infer_version - - # Create pyproject data: setuptools-scm NOT required, no tool section, project with dynamic=['version'] - pyproject_data = PyProjectData.for_testing( - is_required=False, # This is the key: NOT in build-system.requires - section_present=False, - project_present=True, - project_name="test-package-no-scm", - has_dynamic_version=True, - ) - - # Create clean distribution - dist = create_clean_distribution("test-package-no-scm") - - # Call infer_version with direct data injection - should silently return - infer_version(dist, _given_pyproject_data=pyproject_data) - 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) - - import setuptools - - from setuptools_scm._integration.pyproject_reading import PyProjectData - from setuptools_scm._integration.setuptools import version_keyword - - # Create pyproject data: setuptools-scm NOT required, no tool section, project with dynamic=['version'] - pyproject_data = PyProjectData.for_testing( - is_required=False, # This is the key: NOT in build-system.requires - section_present=False, - project_present=True, - project_name="test-package-no-scm", - has_dynamic_version=True, - ) - - # Create distribution - dist = setuptools.Distribution({"name": "test-package-no-scm"}) - - # Call version_keyword with direct data injection - should work regardless of config - version_keyword(dist, "use_scm_version", True, _given_pyproject_data=pyproject_data) - assert dist.metadata.version == "1.0.0" - - -@pytest.mark.xfail( - reason="we currently dont support dynamic version without tool section" -) -def test_verify_dynamic_version_when_required_missing_dynamic() -> None: - """Test that should_infer raises ValueError when setuptools-scm is in build-system.requires but dynamic=['version'] is missing""" - from setuptools_scm._integration.pyproject_reading import PyProjectData - - # Create pyproject data: setuptools-scm required, project present, but no dynamic=['version'] - pyproject_data = PyProjectData.for_testing( - is_required=True, - section_present=False, - project_present=True, - project_name="test-package-missing-dynamic", - has_dynamic_version=False, # This is the key: no dynamic=['version'] - ) - - # should_infer should raise a ValueError when dynamic=['version'] is missing - with pytest.raises( - ValueError, match="dynamic=\\['version'\\] is not set in \\[project\\]" - ): - pyproject_data.should_infer() - - -def test_verify_dynamic_version_when_required_with_tool_section() -> None: - """Test that verification passes when setuptools-scm is in build-system.requires and [tool.setuptools_scm] section exists""" - from setuptools_scm._integration.pyproject_reading import PyProjectData - - # Create pyproject data: setuptools-scm required, tool section present - pyproject_data = PyProjectData.for_testing( - is_required=True, - section_present=True, # This is the key: tool section exists - project_present=True, - project_name="test-package-with-tool-section", - has_dynamic_version=False, # dynamic=['version'] not needed when tool section exists - ) - - assert pyproject_data.is_required is True - assert pyproject_data.section_present is True - - # should_infer should return True because tool section exists - assert pyproject_data.should_infer() is True - - -@pytest.mark.xfail( - reason="we currently dont support dynamic version without tool section" -) -def test_verify_dynamic_version_when_required_with_dynamic() -> None: - """Test that verification passes when setuptools-scm is in build-system.requires and dynamic=['version'] is set""" - from setuptools_scm._integration.pyproject_reading import PyProjectData - - # Create pyproject data: setuptools-scm required, no tool section, but dynamic=['version'] set - pyproject_data = PyProjectData.for_testing( - is_required=True, - section_present=False, - project_present=True, - project_name="test-package-with-dynamic", - has_dynamic_version=True, # This is the key: dynamic=['version'] is set - ) - - assert pyproject_data.is_required is True - assert pyproject_data.section_present is False - - # should_infer should return True because dynamic=['version'] is set - assert pyproject_data.should_infer() is True - - -def test_infer_version_logs_debug_when_missing_dynamic_version( - wd: WorkDir, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test that infer_version gracefully handles and logs debug info when setuptools-scm is in build-system.requires but dynamic=['version'] is missing""" - # Set up a git repository with a tag - wd.commit_testfile("test") - wd("git tag 1.0.0") - monkeypatch.chdir(wd.cwd) - - from setuptools_scm._integration.pyproject_reading import PyProjectData - from setuptools_scm._integration.setuptools import infer_version - - # Create pyproject data: setuptools-scm required, project present, but no dynamic=['version'] - pyproject_data = PyProjectData.for_testing( - is_required=True, - section_present=False, - project_present=True, - project_name="test-package-missing-dynamic", - has_dynamic_version=False, # This is the key: missing dynamic=['version'] - ) - - # Create clean distribution - dist = create_clean_distribution("test-package-missing-dynamic") - - # This should not raise an error, but should silently return (the configuration issue is handled internally) - infer_version(dist, _given_pyproject_data=pyproject_data) - - # 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 @@ -1137,7 +683,6 @@ def test_xmlsec_download_regression( "xmlsec==1.3.16", ], cwd=tmp_path, - text=True, timeout=300, check=True, ) From 206742a617df3922e9a0c3dc36243051e8f2f3c2 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 14 Aug 2025 14:20:23 +0200 Subject: [PATCH 138/162] return simplified activation with a extra to prevent regressions --- CHANGELOG.md | 15 ++++ docs/config.md | 21 +++++- docs/index.md | 21 +++++- docs/usage.md | 54 ++++++++++---- .../_integration/pyproject_reading.py | 73 ++++++++++++++++-- src/setuptools_scm/_requirement_cls.py | 2 + testing/test_integration.py | 14 ++-- testing/test_pyproject_reading.py | 68 +++++++++++++++++ testing/test_version_inference.py | 74 +++++++++++++++++++ 9 files changed, 307 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d96624ca..26a7ea7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # 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 c89a2d24..83d11e2b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2,10 +2,23 @@ ## When is configuration needed? -Starting with setuptools-scm 9.0, explicit configuration is required to enable -version inference, either via the `[tool.setuptools_scm]` section or the -`use_scm_version` setup keyword. Listing `setuptools_scm` in `build-system.requires` -and declaring `project.dynamic = ["version"]` no longer auto-enables inference. +setuptools-scm provides flexible activation options: + +### Simplified Activation (No Configuration Needed) + +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`) 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 a3826727..53f70445 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -7,11 +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 enable `setuptools-scm` at build time: +There are three ways to enable `setuptools-scm` at build time: -### Explicit Configuration (required) +### Simplified Activation (new) -Add a `tool.setuptools_scm` section to explicitly opt-in to version inference: +For basic usage without custom configuration, use the `simple` extra: + +```toml title="pyproject.toml" +[build-system] +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! +``` + +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" + + 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 + + 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] @@ -41,19 +70,16 @@ must ensure build requirements are installed. 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. -!!! warning "Simplified activation removed" - - Previous documentation described a "simplified" activation where listing - `setuptools_scm` in `build-system.requires` together with `project.dynamic = ["version"]` - would auto-enable version inference. This behavior has been removed due to - regressions and ambiguous activation. You must explicitly opt in via either: +!!! note "Legacy simplified activation" - - a `[tool.setuptools_scm]` section, or - - the `use_scm_version` setup keyword. + 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. - The presence of `setuptools_scm` (or `setuptools-scm`) in `build-system.requires` - is still recommended to ensure the dependency is available during builds, but it - no longer enables version inference by itself. + 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/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index f7636d80..f041484f 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -31,6 +31,7 @@ class PyProjectData: is_required: bool section_present: bool project_present: bool + build_requires: list[str] @classmethod def for_testing( @@ -41,6 +42,8 @@ def for_testing( 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 @@ -54,14 +57,22 @@ def for_testing( 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=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 @@ -76,6 +87,7 @@ def empty( is_required=False, section_present=False, project_present=False, + build_requires=[], ) @property @@ -95,14 +107,28 @@ def should_infer(self) -> bool: """ Determine if setuptools_scm should infer version based on configuration. - Only infer when an explicit [tool.setuptools_scm] section is present. - The presence of setuptools-scm in build-system.requires or - project.dynamic does NOT auto-enable inference. + 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 """ - return self.section_present + # 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( @@ -115,6 +141,34 @@ 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 = DEFAULT_PYPROJECT_PATH, tool_name: str = DEFAULT_TOOL_NAME, @@ -160,7 +214,14 @@ def read_pyproject( 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, ) return pyproject_data 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/testing/test_integration.py b/testing/test_integration.py index 1020beb9..dbeb9f87 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -633,14 +633,12 @@ def test_integration_function_call_order( # Create PyProjectData with equivalent configuration - no file I/O! project_name = "test-call-order" - pyproject_data = PyProjectData( - path=Path("pyproject.toml"), - tool_name="setuptools_scm", - project={"name": project_name, "dynamic": ["version"]}, - section={"local_scheme": "no-local-version"}, # [tool.setuptools_scm] config - is_required=True, # setuptools_scm in build-system.requires - section_present=True, # [tool.setuptools_scm] section exists - project_present=True, # [project] section exists + pyproject_data = PyProjectData.for_testing( + project_name=project_name, + has_dynamic_version=True, + project_present=True, + section_present=True, + local_scheme="no-local-version", ) dist = create_clean_distribution(project_name) diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py index bd906bd8..1962882a 100644 --- a/testing/test_pyproject_reading.py +++ b/testing/test_pyproject_reading.py @@ -4,6 +4,7 @@ import pytest +from setuptools_scm._integration.pyproject_reading import has_build_package_with_extra from setuptools_scm._integration.pyproject_reading import read_pyproject @@ -40,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 24a1c82e..967ab768 100644 --- a/testing/test_version_inference.py +++ b/testing/test_version_inference.py @@ -179,3 +179,77 @@ def test_tool_section_present(self) -> None: pyproject_data=PYPROJECT.WITHOUT_PROJECT, expected=VersionInferenceConfig, ) + + 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=pyproject_data, + expected=VersionInferenceConfig, + ) + + 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=pyproject_data, + expected=NOOP, + ) + + 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=pyproject_data, + expected=NOOP, + ) + + 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]"], + ) + expect_config( + current_version=None, + pyproject_data=pyproject_data, + expected=NOOP, + ) + + 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, + expected=WARNING_PACKAGE, + ) From 56a7a00ad4b33ab1835f6768736388ea5c18c0a7 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 16 Aug 2025 13:51:24 +0200 Subject: [PATCH 139/162] split version inference into the part that needs a distribution object and the part that doesnt --- .../_integration/version_inference.py | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py index cbfe9f61..6258d90b 100644 --- a/src/setuptools_scm/_integration/version_inference.py +++ b/src/setuptools_scm/_integration/version_inference.py @@ -25,22 +25,13 @@ class VersionInferenceConfig: 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 - - config = _config_module.Configuration.from_file( - dist_name=self.dist_name, - pyproject_data=self.pyproject_data, - **(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: - 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: @@ -75,6 +66,43 @@ def apply(self, dist: Distribution) -> None: ] +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, From 250b5c2143c34174f5c855281f51499c568ebee8 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 16 Aug 2025 14:28:42 +0200 Subject: [PATCH 140/162] add test migration plan for llms + fix changelog formatting --- CHANGELOG.md | 1 + testing/INTEGRATION_MIGRATION_PLAN.md | 92 +++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 testing/INTEGRATION_MIGRATION_PLAN.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a7ea7a..6b636da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### removed + - unchecked simplified activation - too many projects use setups where it would fail 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. + + From b8a2920a2441232a74ab33bf028a39e140eccc86 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 16 Aug 2025 14:33:33 +0200 Subject: [PATCH 141/162] Add simple extra to pyproject.toml Its used when using the simplified activation. --- pyproject.toml | 1 + testing/test_integration.py | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8fbd0dbb..d6005cd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ dependencies = [ ] [project.optional-dependencies] rich = ["rich"] +simple = [] toml = [] [dependency-groups] diff --git a/testing/test_integration.py b/testing/test_integration.py index dbeb9f87..be6e3cfe 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -418,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) diff --git a/uv.lock b/uv.lock index 996e30c3..4b145a91 100644 --- a/uv.lock +++ b/uv.lock @@ -1870,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 = [ From c49467d206ee25dc4c415d5ea9f6e4c2238d6a6e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 16 Aug 2025 14:48:22 +0200 Subject: [PATCH 142/162] Update CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b636da4..caf73ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## v9.2.0 ### Added @@ -15,6 +15,9 @@ - unchecked simplified activation - too many projects use setups where it would fail +### changed + +- refine activation logic and add unittest for the relevant cases instead of trying to speedrun setuptools ## v9.1.1 From 27e3b7a3e8de2e7717ab2ce4ecdd2dafe1c1dc35 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 17 Aug 2025 15:57:41 +0200 Subject: [PATCH 143/162] docs: Remove duplicate changelog entry for v9.1.1. --- CHANGELOG.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caf73ad3..b95362e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,13 +26,6 @@ - fix #1194: correctly handle version keyword when pyproject metadata is missing -## v9.1.1 - -### fixed - -- fix #1194: correctly handle version keyword when pyproject metadata is missing - - ## v9.1.0 ### fixed From 829f25a264325195989d6fb173ee85b89568b3f9 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 17 Aug 2025 15:58:41 +0200 Subject: [PATCH 144/162] docs: Mark v9.1.0 and v9.1.1 as yanked in the changelog. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b95362e4..ce8fd494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,14 +19,14 @@ - refine activation logic and add unittest for the relevant cases instead of trying to speedrun setuptools -## v9.1.1 +## v9.1.1 (yanked) ### fixed - fix #1194: correctly handle version keyword when pyproject metadata is missing -## v9.1.0 +## v9.1.0 (yanked) ### fixed From 0207ee91237b8559186a14f52a72930d6cdb214c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:00:14 +0000 Subject: [PATCH 145/162] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce8fd494..1e1a9961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ ### changed -- refine activation logic and add unittest for the relevant cases instead of trying to speedrun setuptools +- refine activation logic and add unittest for the relevant cases instead of trying to speedrun setuptools ## v9.1.1 (yanked) From 1237bdfd0097865478cbbc4867fcd239dec4a1c8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 18:31:26 +0000 Subject: [PATCH 146/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.8 → v0.12.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.8...v0.12.12) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2d32f12..34b550a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.8 + rev: v0.12.12 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix, --show-fixes] From 0718f7cd67c4fba91187212c637b70a38c0e87e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:01:41 +0000 Subject: [PATCH 147/162] Bump actions/setup-python from 5 to 6 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/api-check.yml | 2 +- .github/workflows/python-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml index c0066353..ffd6d3e9 100644 --- a/.github/workflows/api-check.yml +++ b/.github/workflows/api-check.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 0fc255ca..5e2e468b 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -54,7 +54,7 @@ jobs: with: fetch-depth: 0 - name: Setup python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 if: matrix.python_version != 'msys2' with: python-version: ${{ matrix.python_version }} From dafb1c3ada9e1fcccccb21db0bd9c53181c89cb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:01:44 +0000 Subject: [PATCH 148/162] Bump actions/github-script from 7 to 8 Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/api-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml index c0066353..d55ffac0 100644 --- a/.github/workflows/api-check.yml +++ b/.github/workflows/api-check.yml @@ -45,7 +45,7 @@ jobs: - name: Report API check result if: steps.griffe-check.outputs.api_check_result == 'warning' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | core.warning('API stability check detected breaking changes. Please review the API changes above.') From 6398650631169749115fc3b058ca9c96eff6cc57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 18:31:05 +0000 Subject: [PATCH 149/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.12 → v0.13.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.12...v0.13.0) - [github.com/pre-commit/mirrors-mypy: v1.17.1 → v1.18.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.1...v1.18.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34b550a9..23e3794f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.12 + rev: v0.13.0 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.18.1 hooks: - id: mypy args: [--strict] From 6a9de128d8d4de3fb86bd43312b83b745bfe0722 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 19 Aug 2025 17:39:28 +0200 Subject: [PATCH 150/162] ignore dynamic version in setup.cfg partial solution to #1216 --- CHANGELOG.md | 9 ++++++++ src/setuptools_scm/_integration/setup_cfg.py | 7 ++++++ testing/test_integration.py | 24 ++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1a9961..87fa9dc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog + +## v9.2.0 + +### fixed + +- fix #1216: accept and create a warning for usages of `version = attr:` in setuptools config. + unfortunately dozens of projects cargo-culted that antipattern + + ## v9.2.0 ### Added diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/src/setuptools_scm/_integration/setup_cfg.py index 4e485600..7a06f934 100644 --- a/src/setuptools_scm/_integration/setup_cfg.py +++ b/src/setuptools_scm/_integration/setup_cfg.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import warnings from dataclasses import dataclass from pathlib import Path @@ -25,6 +26,12 @@ def read_setup_cfg(input: str | os.PathLike[str] = "setup.cfg") -> SetuptoolsBas name = parser.get("metadata", "name", fallback=None) version = parser.get("metadata", "version", fallback=None) + if version is not None and "attr" in version: + warnings.warn( + "setup.cfg: ignoring invalid dynamic version - version = attr: ..." + " is sabotaging setuptools-scm" + ) + version = None return SetuptoolsBasicData(path=path, name=name, version=version) diff --git a/testing/test_integration.py b/testing/test_integration.py index be6e3cfe..387c0635 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -18,6 +18,7 @@ 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._integration.setup_cfg import read_setup_cfg from setuptools_scm._requirement_cls import extract_package_name if TYPE_CHECKING: @@ -457,6 +458,29 @@ def test_unicode_in_setup_cfg(tmp_path: Path) -> None: assert data.version == "1.2.3" +@pytest.mark.issue(1216) +def test_setup_cfg_dynamic_version_warns_and_ignores(tmp_path: Path) -> None: + cfg = tmp_path / "setup.cfg" + cfg.write_text( + textwrap.dedent( + """ + [metadata] + name = example-broken + version = attr: example_broken.__version__ + """ + ), + encoding="utf-8", + ) + + with pytest.warns( + UserWarning, + match="setup.cfg: ignoring invalid dynamic version - version = attr: ... is sabotaging setuptools-scm", + ): + legacy_data = read_setup_cfg(cfg) + + assert legacy_data.version is None + + def test_setup_cfg_version_prevents_inference_version_keyword( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: From e922ebe7b59123aedbd170e3185cc1a051705296 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 19 Aug 2025 17:45:58 +0200 Subject: [PATCH 151/162] test instrumentation: add dependency injected toml content to read_pyproject --- .../_integration/pyproject_reading.py | 6 +++++- testing/test_pyproject_reading.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index f041484f..e35aac45 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -174,6 +174,7 @@ def read_pyproject( tool_name: str = DEFAULT_TOOL_NAME, canonical_build_package_name: str = "setuptools-scm", _given_result: _t.GivenPyProjectResult = None, + _given_definition: TOML_RESULT | None = None, ) -> PyProjectData: """Read and parse pyproject configuration. @@ -195,7 +196,10 @@ def read_pyproject( if isinstance(_given_result, (InvalidTomlError, FileNotFoundError)): raise _given_result - defn = read_toml_content(path) + if _given_definition is not None: + defn = _given_definition + else: + defn = read_toml_content(path) requires: list[str] = defn.get("build-system", {}).get("requires", []) is_required = has_build_package(requires, canonical_build_package_name) diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py index 1962882a..d625bcdd 100644 --- a/testing/test_pyproject_reading.py +++ b/testing/test_pyproject_reading.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from unittest.mock import Mock import pytest @@ -108,3 +109,20 @@ def test_invalid_requirement_string(self) -> None: assert ( has_build_package_with_extra(requires, "setuptools-scm", "simple") is False ) + + +def test_read_pyproject_with_given_definition(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that read_pyproject reads existing files correctly.""" + monkeypatch.setattr( + "setuptools_scm._integration.pyproject_reading.read_toml_content", + Mock(side_effect=FileNotFoundError("this test should not read")), + ) + + res = read_pyproject( + _given_definition={ + "build-system": {"requires": ["setuptools-scm[simple]"]}, + "project": {"name": "test-package", "dynamic": ["version"]}, + } + ) + + assert res.should_infer() From 34b72e1f954cbb75b57d0c1c25679582b2a00f73 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 19 Aug 2025 17:54:19 +0200 Subject: [PATCH 152/162] add a warning when setuptools dynamic version in pyproject is used in error --- src/setuptools_scm/_config.py | 1 - .../_integration/pyproject_reading.py | 12 +++++++++++ testing/test_pyproject_reading.py | 20 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 81a78e84..49fac2a4 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -310,7 +310,6 @@ def from_data( # Handle nested SCM configuration scm_config = ScmConfiguration.from_data(scm_data) - return cls( relative_to=relative_to, version_cls=version_cls, diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index e35aac45..0716df70 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -228,6 +228,18 @@ def read_pyproject( requires, ) + setuptools_dynamic_version = ( + defn.get("tool", {}) + .get("setuptools", {}) + .get("dynamic", {}) + .get("version", None) + ) + if setuptools_dynamic_version is not None: + warnings.warn( + f"{path}: at [tool.setuptools.dynamic]\n" + "version = {attr = ...} is sabotaging setuptools-scm" + ) + return pyproject_data diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py index d625bcdd..d6b6705e 100644 --- a/testing/test_pyproject_reading.py +++ b/testing/test_pyproject_reading.py @@ -126,3 +126,23 @@ def test_read_pyproject_with_given_definition(monkeypatch: pytest.MonkeyPatch) - ) assert res.should_infer() + + +def test_read_pyproject_with_setuptools_dynamic_version_warns() -> None: + with pytest.warns( + UserWarning, + match=r"pyproject.toml: at \[tool\.setuptools\.dynamic\]\n" + r"version = {attr = \.\.\.} is sabotaging setuptools-scm", + ): + pyproject_data = read_pyproject( + _given_definition={ + "build-system": {"requires": ["setuptools-scm[simple]"]}, + "project": {"name": "test-package", "dynamic": ["version"]}, + "tool": { + "setuptools": { + "dynamic": {"version": {"attr": "test_package.__version__"}} + } + }, + } + ) + assert pyproject_data.project_version is None From ec99afe8b8c4b4d8585f23a0cf5573186b611325 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Aug 2025 12:36:54 +0200 Subject: [PATCH 153/162] use a actually user friendly deprecation for the setuptools dynamic antipattern --- .../_integration/deprecation.py | 20 ++++++++++++++++ .../_integration/pyproject_reading.py | 7 +++--- src/setuptools_scm/_integration/setup_cfg.py | 8 +++---- testing/test_deprecation.py | 24 +++++++++++++++++++ testing/test_integration.py | 2 +- testing/test_pyproject_reading.py | 3 +-- 6 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 src/setuptools_scm/_integration/deprecation.py create mode 100644 testing/test_deprecation.py diff --git a/src/setuptools_scm/_integration/deprecation.py b/src/setuptools_scm/_integration/deprecation.py new file mode 100644 index 00000000..efb5e372 --- /dev/null +++ b/src/setuptools_scm/_integration/deprecation.py @@ -0,0 +1,20 @@ +import warnings + +from pathlib import Path + + +def warn_dynamic_version(path: Path, section: str, expression: str) -> None: + warnings.warn( + f"{path}: at [{section}]\n" + f"{expression} forcing setuptools to override the version setuptools-scm sets\n" + "When using setuptools-scm its invalid to use setuptools dynamic version as well, please removeit.\n" + "Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors." + ) + + +def warn_pyproject_setuptools_dynamic_version(path: Path) -> None: + warn_dynamic_version(path, "tool.setuptools.dynamic", "version = {attr = ...}") + + +def warn_setup_cfg_dynamic_version(path: Path) -> None: + warn_dynamic_version(path, "metadata", "version = attr: ...}") diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 0716df70..58abce74 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -235,10 +235,9 @@ def read_pyproject( .get("version", None) ) if setuptools_dynamic_version is not None: - warnings.warn( - f"{path}: at [tool.setuptools.dynamic]\n" - "version = {attr = ...} is sabotaging setuptools-scm" - ) + from .deprecation import warn_pyproject_setuptools_dynamic_version + + warn_pyproject_setuptools_dynamic_version(path) return pyproject_data diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/src/setuptools_scm/_integration/setup_cfg.py index 7a06f934..893a9ad4 100644 --- a/src/setuptools_scm/_integration/setup_cfg.py +++ b/src/setuptools_scm/_integration/setup_cfg.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import warnings from dataclasses import dataclass from pathlib import Path @@ -27,10 +26,9 @@ def read_setup_cfg(input: str | os.PathLike[str] = "setup.cfg") -> SetuptoolsBas name = parser.get("metadata", "name", fallback=None) version = parser.get("metadata", "version", fallback=None) if version is not None and "attr" in version: - warnings.warn( - "setup.cfg: ignoring invalid dynamic version - version = attr: ..." - " is sabotaging setuptools-scm" - ) + from .deprecation import warn_setup_cfg_dynamic_version + + warn_setup_cfg_dynamic_version(path) version = None return SetuptoolsBasicData(path=path, name=name, version=version) diff --git a/testing/test_deprecation.py b/testing/test_deprecation.py new file mode 100644 index 00000000..d738eb55 --- /dev/null +++ b/testing/test_deprecation.py @@ -0,0 +1,24 @@ +"""Test deprecation warnings and their exact text.""" + +from pathlib import Path + +import pytest + +from setuptools_scm._integration.deprecation import warn_dynamic_version + + +def test_warn_dynamic_version_full_text() -> None: + """Test the complete warning text for warn_dynamic_version function.""" + test_path = Path("test_file.toml") + expected_warning = ( + f"{test_path}: at [test.section]\n" + "test_expression forcing setuptools to override the version setuptools-scm sets\n" + "When using setuptools-scm its invalid to use setuptools dynamic version as well, please removeit.\n" + "Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors." + ) + + with pytest.warns(UserWarning) as warning_info: # noqa: PT030 + warn_dynamic_version(test_path, "test.section", "test_expression") + + assert len(warning_info) == 1 + assert str(warning_info[0].message) == expected_warning diff --git a/testing/test_integration.py b/testing/test_integration.py index 387c0635..e85b5bba 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -474,7 +474,7 @@ def test_setup_cfg_dynamic_version_warns_and_ignores(tmp_path: Path) -> None: with pytest.warns( UserWarning, - match="setup.cfg: ignoring invalid dynamic version - version = attr: ... is sabotaging setuptools-scm", + match=r"setup\.cfg: at \[metadata\]", ): legacy_data = read_setup_cfg(cfg) diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py index d6b6705e..dc26e955 100644 --- a/testing/test_pyproject_reading.py +++ b/testing/test_pyproject_reading.py @@ -131,8 +131,7 @@ def test_read_pyproject_with_given_definition(monkeypatch: pytest.MonkeyPatch) - def test_read_pyproject_with_setuptools_dynamic_version_warns() -> None: with pytest.warns( UserWarning, - match=r"pyproject.toml: at \[tool\.setuptools\.dynamic\]\n" - r"version = {attr = \.\.\.} is sabotaging setuptools-scm", + match=r"pyproject\.toml: at \[tool\.setuptools\.dynamic\]", ): pyproject_data = read_pyproject( _given_definition={ From 343f79149e837fa5d9ffa72ae1db2c42ee621a57 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 12 Oct 2025 11:02:13 +0200 Subject: [PATCH 154/162] Improve read_pyproject docstring with reStructuredText format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert docstring to reStructuredText format and document both test hooks (_given_result and _given_definition). Clarifies that _given_definition is ignored when _given_result is provided. Note: Python 3.10 pin removed from pre-commit configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .pre-commit-config.yaml | 1 - .../_integration/pyproject_reading.py | 23 +++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23e3794f..1423942d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,6 @@ repos: hooks: - id: mypy args: [--strict] - language_version: "3.10" additional_dependencies: - types-setuptools - tokenize-rt==3.2.0 diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 58abce74..eb21dfa4 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -178,16 +178,19 @@ def read_pyproject( ) -> PyProjectData: """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 + This function supports dependency injection for tests via ``_given_result`` + and ``_given_definition``. + + :param path: Path to the pyproject file + :param tool_name: The tool section name (default: ``setuptools_scm``) + :param canonical_build_package_name: Normalized build requirement name + :param _given_result: Optional testing hook. Can be: + - ``PyProjectData``: returned directly + - ``InvalidTomlError`` | ``FileNotFoundError``: raised directly + - ``None``: read from filesystem (default) + :param _given_definition: Optional testing hook to provide parsed TOML content. + When provided, this dictionary is used instead of reading and parsing + the file from disk. Ignored if ``_given_result`` is provided. """ if _given_result is not None: From 39699826206fc9fbaa5564faeb498afcbcd1a24f Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 12 Oct 2025 11:08:57 +0200 Subject: [PATCH 155/162] Fix typos and grammar in deprecation warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix "removeit" → "remove it" (missing space) - Fix "its invalid" → "it's invalid" (missing apostrophe) - Remove extra closing brace from setup.cfg expression - Update test to match the actual implementation text 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/setuptools_scm/_integration/deprecation.py | 6 +++--- testing/test_deprecation.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/setuptools_scm/_integration/deprecation.py b/src/setuptools_scm/_integration/deprecation.py index efb5e372..a1b36155 100644 --- a/src/setuptools_scm/_integration/deprecation.py +++ b/src/setuptools_scm/_integration/deprecation.py @@ -6,8 +6,8 @@ def warn_dynamic_version(path: Path, section: str, expression: str) -> None: warnings.warn( f"{path}: at [{section}]\n" - f"{expression} forcing setuptools to override the version setuptools-scm sets\n" - "When using setuptools-scm its invalid to use setuptools dynamic version as well, please removeit.\n" + f"{expression} is forcing setuptools to override the version setuptools-scm did already set\n" + "When using setuptools-scm it's invalid to use setuptools dynamic version as well, please remove it.\n" "Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors." ) @@ -17,4 +17,4 @@ def warn_pyproject_setuptools_dynamic_version(path: Path) -> None: def warn_setup_cfg_dynamic_version(path: Path) -> None: - warn_dynamic_version(path, "metadata", "version = attr: ...}") + warn_dynamic_version(path, "metadata", "version = attr: ...") diff --git a/testing/test_deprecation.py b/testing/test_deprecation.py index d738eb55..fa6e5aaf 100644 --- a/testing/test_deprecation.py +++ b/testing/test_deprecation.py @@ -12,8 +12,8 @@ def test_warn_dynamic_version_full_text() -> None: test_path = Path("test_file.toml") expected_warning = ( f"{test_path}: at [test.section]\n" - "test_expression forcing setuptools to override the version setuptools-scm sets\n" - "When using setuptools-scm its invalid to use setuptools dynamic version as well, please removeit.\n" + "test_expression is forcing setuptools to override the version setuptools-scm did already set\n" + "When using setuptools-scm it's invalid to use setuptools dynamic version as well, please remove it.\n" "Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors." ) From 536fe21b0ce40c722d67d893a2269e48378fee44 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 12 Oct 2025 11:14:34 +0200 Subject: [PATCH 156/162] Fix duplicate v9.2.0 changelog entry and improve consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename first v9.2.0 to v9.2.1 (unreleased) to distinguish from released v9.2.0 - Standardize section header capitalization (Fixed, Added, Changed, Removed) - Add missing # to issue reference in v9.0.3 (#1184) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87fa9dc1..1dc4debb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog -## v9.2.0 +## v9.2.1 (unreleased) -### fixed +### Fixed - fix #1216: accept and create a warning for usages of `version = attr:` in setuptools config. unfortunately dozens of projects cargo-culted that antipattern @@ -20,24 +20,24 @@ version inference is automatically enabled with default settings. -### removed +### Removed - unchecked simplified activation - too many projects use setups where it would fail -### changed +### Changed - refine activation logic and add unittest for the relevant cases instead of trying to speedrun setuptools ## v9.1.1 (yanked) -### fixed +### Fixed - fix #1194: correctly handle version keyword when pyproject metadata is missing ## v9.1.0 (yanked) -### fixed +### Fixed - complete reiteration of the decision logic for enabling version inference on setuptools_scm @@ -49,9 +49,9 @@ ## v9.0.3 (yanked) -### fixed +### Fixed -- fix 1184: verify version is dynamic if the dependency is used as indicator for enabling +- fix #1184: verify version is dynamic if the dependency is used as indicator for enabling ## v9.0.2 (yanked) From 8c5cec917123e4cc87b490f56b6e7af27a3b0399 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 12 Oct 2025 11:25:45 +0200 Subject: [PATCH 157/162] Fix API stability check workflow to install griffe and improve reporting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The griffe command was failing silently because griffe was not installed. This commit fixes the workflow to properly install griffe and improves error reporting. Changes: - Add explicit griffe installation step - Add continue-on-error to capture exit codes - Consolidate reporting into single step with if: always() - Handle three states: success, warning (breaking changes), and failure - Capture and report exit codes for better debugging - Add proper GitHub notices, warnings, and summaries for all states 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/api-check.yml | 36 ++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml index 899e645e..4db2526b 100644 --- a/.github/workflows/api-check.yml +++ b/.github/workflows/api-check.yml @@ -32,22 +32,48 @@ jobs: run: | pip install -U pip setuptools pip install -e .[test] + pip install griffe - name: Run griffe API check id: griffe-check + continue-on-error: true run: | + echo "Running griffe API stability check..." if griffe check setuptools_scm -ssrc -f github; then echo "api_check_result=success" >> $GITHUB_OUTPUT + echo "exit_code=0" >> $GITHUB_OUTPUT else + exit_code=$? echo "api_check_result=warning" >> $GITHUB_OUTPUT - echo "API stability check detected changes but will not fail the build" >> $GITHUB_STEP_SUMMARY + echo "exit_code=$exit_code" >> $GITHUB_OUTPUT + exit $exit_code fi - name: Report API check result - if: steps.griffe-check.outputs.api_check_result == 'warning' + if: always() uses: actions/github-script@v8 with: script: | - core.warning('API stability check detected breaking changes. Please review the API changes above.') - core.summary.addRaw('⚠️ API Stability Warning: Breaking changes detected in the public API') - await core.summary.write() \ No newline at end of file + const result = '${{ steps.griffe-check.outputs.api_check_result }}' + const exitCode = '${{ steps.griffe-check.outputs.exit_code }}' + + if (result === 'success') { + core.notice('API stability check passed - no breaking changes detected') + await core.summary + .addHeading('✅ API Stability Check: Passed', 2) + .addRaw('No breaking changes detected in the public API') + .write() + } else if (result === 'warning') { + core.warning(`API stability check detected breaking changes (exit code: ${exitCode}). Please review the API changes above.`) + await core.summary + .addHeading('⚠️ API Stability Warning', 2) + .addRaw('Breaking changes detected in the public API. Please review the changes reported above.') + .addRaw(`\n\nExit code: ${exitCode}`) + .write() + } else { + core.error('API stability check failed to run properly') + await core.summary + .addHeading('❌ API Stability Check: Failed', 2) + .addRaw('The griffe check failed to execute. This may indicate griffe is not installed or there was an error.') + .write() + } \ No newline at end of file From 14d85c0fb8fa9efd1ec4bac3413f48aea0e319f5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 12 Oct 2025 11:32:37 +0200 Subject: [PATCH 158/162] Install Mercurial on Windows runners via Chocolatey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows runners no longer ship with Mercurial pre-installed, causing test failures. This commit adds Mercurial installation using Chocolatey in the existing GnuPG setup step. Changes: - Add mercurial to the choco install command alongside gnupg - Update step name to reflect it now installs both GnuPG and Mercurial - Add explanatory comment about why Mercurial installation is needed This uses a modern, declarative approach via the Chocolatey package manager that's already in use for GnuPG installation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/python-tests.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 5e2e468b..17953d55 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -68,7 +68,7 @@ jobs: msystem: MINGW64 install: git mingw-w64-x86_64-python mingw-w64-x86_64-python-setuptools update: true - - name: Setup GnuPG + - name: Setup GnuPG and Mercurial on Windows # At present, the Windows VMs only come with the copy of GnuPG that's bundled # with Git for Windows. If we want to use this version _and_ be able to set # arbitrary GnuPG home directories, then the test would need to figure out when @@ -84,11 +84,15 @@ jobs: # Additionally, we'll explicitly set `gpg.program` to ensure Git for Windows # doesn't invoke the bundled GnuPG, otherwise we'll run into # . See also: . + # + # Windows runners no longer ship with Mercurial pre-installed, so we install + # it via Chocolatey using the 'hg' package. run: | $env:PATH = "C:\Program Files\Git\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\ProgramData\Chocolatey\bin" [Environment]::SetEnvironmentVariable("Path", $env:PATH, "Machine") - choco install gnupg -y --no-progress + choco install gnupg hg -y --no-progress echo "C:\Program Files (x86)\gnupg\bin" >> $env:GITHUB_PATH + echo "C:\Program Files\Mercurial\" >> $env:GITHUB_PATH git config --system gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe" if: runner.os == 'Windows' - run: uv sync --group test --group docs --extra rich From 20a44648f299f64b1241a448bdbafde9b1871ebf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:05:32 +0000 Subject: [PATCH 159/162] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.0 → v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.0...v0.13.3) - [github.com/pre-commit/mirrors-mypy: v1.18.1 → v1.18.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.18.1...v1.18.2) - [github.com/scientific-python/cookie: 2025.05.02 → 2025.10.01](https://github.com/scientific-python/cookie/compare/2025.05.02...2025.10.01) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1423942d..5f66a9f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.0 + rev: v0.13.3 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.1 + rev: v1.18.2 hooks: - id: mypy args: [--strict] @@ -27,7 +27,7 @@ repos: - rich - repo: https://github.com/scientific-python/cookie - rev: 2025.05.02 + rev: 2025.10.01 hooks: - id: sp-repo-review From a893634c36a074052ed3f6d0e77b076b582c72ab Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 12 Oct 2025 12:25:38 +0200 Subject: [PATCH 160/162] Prepare release v9.2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove (unreleased) marker from v9.2.1 in CHANGELOG.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc4debb..a8d8964f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog -## v9.2.1 (unreleased) +## v9.2.1 ### Fixed From 95a0c47553ea0b373238bb64aa344b2c2547c849 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 19 Oct 2025 22:46:18 +0200 Subject: [PATCH 161/162] fix: don't warn about tool.setuptools.dynamic.version when only using file finder When setuptools-scm is used only for its file finder functionality (no version inference), it's valid to use tool.setuptools.dynamic.version for versioning. The warning should only be issued when setuptools-scm is actually performing version inference. Changes: - Modified pyproject_reading.py to only warn when should_infer() is True - Added test for file-finder-only case (no warning expected) - Updated existing test with clarifying documentation Fixes #1231 --- .../_integration/pyproject_reading.py | 5 ++- testing/test_pyproject_reading.py | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index eb21dfa4..75d86f62 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -237,7 +237,10 @@ def read_pyproject( .get("dynamic", {}) .get("version", None) ) - if setuptools_dynamic_version is not None: + # Only warn if setuptools-scm is being used for version inference + # (not just file finding). When only file finders are used, it's valid + # to use tool.setuptools.dynamic.version for versioning. + if setuptools_dynamic_version is not None and pyproject_data.should_infer(): from .deprecation import warn_pyproject_setuptools_dynamic_version warn_pyproject_setuptools_dynamic_version(path) diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py index dc26e955..2a1fa89b 100644 --- a/testing/test_pyproject_reading.py +++ b/testing/test_pyproject_reading.py @@ -129,6 +129,7 @@ def test_read_pyproject_with_given_definition(monkeypatch: pytest.MonkeyPatch) - def test_read_pyproject_with_setuptools_dynamic_version_warns() -> None: + """Test that warning is issued when version inference is enabled.""" with pytest.warns( UserWarning, match=r"pyproject\.toml: at \[tool\.setuptools\.dynamic\]", @@ -145,3 +146,36 @@ def test_read_pyproject_with_setuptools_dynamic_version_warns() -> None: } ) assert pyproject_data.project_version is None + + +def test_read_pyproject_with_setuptools_dynamic_version_no_warn_when_file_finder_only() -> ( + None +): + """Test that no warning is issued when only file finder is used (no version inference).""" + # When setuptools-scm is used only for file finding (no [tool.setuptools_scm] section, + # no [simple] extra, version not in dynamic), it's valid to use tool.setuptools.dynamic.version + import warnings + + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + pyproject_data = read_pyproject( + _given_definition={ + "build-system": {"requires": ["setuptools-scm"]}, + "project": {"name": "test-package", "version": "1.0.0"}, + "tool": { + "setuptools": { + "dynamic": {"version": {"attr": "test_package.__version__"}} + } + }, + } + ) + + # Filter to check for the dynamic version warning specifically + relevant_warnings = [ + w for w in warning_list if "tool.setuptools.dynamic" in str(w.message) + ] + assert len(relevant_warnings) == 0, ( + "Should not warn about tool.setuptools.dynamic when only using file finder" + ) + assert pyproject_data.project_version == "1.0.0" + assert not pyproject_data.should_infer() From 4f55e9585e398e13103112a6fd488109d9da4ead Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 19 Oct 2025 22:48:46 +0200 Subject: [PATCH 162/162] docs: update changelog for v9.2.2 patch release --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d8964f..b588430e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog +## v9.2.2 + +### Fixed + +- fix #1231: don't warn about `tool.setuptools.dynamic.version` when only using file finder. + The warning about combining version guessing with setuptools dynamic versions should only + be issued when setuptools-scm is performing version inference, not when it's only being + used for its file finder functionality. + + ## v9.2.1 ### Fixed