Skip to content

Commit 402fc25

Browse files
turn the version inference config into a protocol and simplify the setuptools integration points
- test fixes : make clean distribution objects
1 parent 8e17796 commit 402fc25

File tree

4 files changed

+137
-107
lines changed

4 files changed

+137
-107
lines changed

src/setuptools_scm/_integration/setuptools.py

Lines changed: 15 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,13 @@
33
import logging
44
import warnings
55

6-
from pathlib import Path
76
from typing import Any
87
from typing import Callable
98

109
import setuptools
1110

12-
from .. import _config
1311
from .pyproject_reading import read_pyproject
1412
from .setup_cfg import _dist_name_from_legacy
15-
from .version_inference import VersionInferenceConfig
16-
from .version_inference import VersionInferenceError
17-
from .version_inference import VersionInferenceException
1813
from .version_inference import get_version_inference_config
1914

2015
log = logging.getLogger(__name__)
@@ -39,22 +34,6 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None:
3934
)
4035

4136

42-
def _assign_version(
43-
dist: setuptools.Distribution, config: _config.Configuration
44-
) -> None:
45-
from .._get_version_impl import _get_version
46-
from .._get_version_impl import _version_missing
47-
48-
# todo: build time plugin
49-
maybe_version = _get_version(config, force_write_version_files=True)
50-
51-
if maybe_version is None:
52-
_version_missing(config)
53-
else:
54-
assert dist.metadata.version is None
55-
dist.metadata.version = maybe_version
56-
57-
5837
_warn_on_old_setuptools()
5938

6039

@@ -80,6 +59,10 @@ def version_keyword(
8059
keyword: str,
8160
value: bool | dict[str, Any] | Callable[[], dict[str, Any]],
8261
) -> None:
62+
"""apply version infernce when setup(use_scm_version=...) is used
63+
this takes priority over the finalize_options based version
64+
"""
65+
8366
_log_hookstart("version_keyword", dist)
8467

8568
# Parse overrides (integration point responsibility)
@@ -95,14 +78,11 @@ def version_keyword(
9578

9679
# Get pyproject data
9780
try:
98-
pyproject_data = read_pyproject(
99-
Path("pyproject.toml"), missing_section_ok=True, missing_file_ok=True
100-
)
81+
pyproject_data = read_pyproject(missing_section_ok=True, missing_file_ok=True)
10182
except (LookupError, ValueError) as e:
10283
log.debug("Configuration issue in pyproject.toml: %s", e)
10384
return
10485

105-
# Get decision
10686
result = get_version_inference_config(
10787
dist_name=dist_name,
10888
current_version=dist.metadata.version,
@@ -111,72 +91,33 @@ def version_keyword(
11191
was_set_by_infer=was_set_by_infer,
11292
)
11393

114-
# Handle result
115-
if result is None:
116-
return # Don't infer
117-
elif isinstance(result, VersionInferenceError):
118-
if result.should_warn:
119-
warnings.warn(result.message)
120-
return
121-
elif isinstance(result, VersionInferenceException):
122-
raise result.exception
123-
elif isinstance(result, VersionInferenceConfig):
124-
# Clear version if it was set by infer_version
125-
if was_set_by_infer:
126-
dist._setuptools_scm_version_set_by_infer = False # type: ignore[attr-defined]
127-
dist.metadata.version = None
128-
129-
# Proceed with inference
130-
config = _config.Configuration.from_file(
131-
dist_name=result.dist_name,
132-
pyproject_data=result.pyproject_data,
133-
missing_file_ok=True,
134-
missing_section_ok=True,
135-
**overrides,
136-
)
137-
_assign_version(dist, config)
94+
result.apply(dist)
13895

13996

14097
def infer_version(dist: setuptools.Distribution) -> None:
98+
"""apply version inference from the finalize_options hook
99+
this is the default for pyproject.toml based projects that don't use the use_scm_version keyword
100+
101+
if the version keyword is used, it will override the version from this hook
102+
as user might have passed custom code version schemes
103+
"""
104+
141105
_log_hookstart("infer_version", dist)
142106

143107
dist_name = _dist_name_from_legacy(dist)
144108

145-
# Get pyproject data (integration point responsibility)
146109
try:
147-
pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True)
110+
pyproject_data = read_pyproject(missing_section_ok=True)
148111
except FileNotFoundError:
149112
log.debug("pyproject.toml not found, skipping infer_version")
150113
return
151114
except (LookupError, ValueError) as e:
152115
log.debug("Configuration issue in pyproject.toml: %s", e)
153116
return
154117

155-
# Get decision
156118
result = get_version_inference_config(
157119
dist_name=dist_name,
158120
current_version=dist.metadata.version,
159121
pyproject_data=pyproject_data,
160122
)
161-
162-
# Handle result
163-
if result is None:
164-
return # Don't infer
165-
elif isinstance(result, VersionInferenceError):
166-
if result.should_warn:
167-
log.warning(result.message)
168-
return
169-
elif isinstance(result, VersionInferenceException):
170-
raise result.exception
171-
elif isinstance(result, VersionInferenceConfig):
172-
# Proceed with inference
173-
try:
174-
config = _config.Configuration.from_file(
175-
dist_name=result.dist_name, pyproject_data=result.pyproject_data
176-
)
177-
except LookupError as e:
178-
log.info(e, exc_info=True)
179-
else:
180-
_assign_version(dist, config)
181-
# Mark that this version was set by infer_version
182-
dist._setuptools_scm_version_set_by_infer = True # type: ignore[attr-defined]
123+
result.apply(dist)

src/setuptools_scm/_integration/version_inference.py

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,40 @@ class VersionInferenceConfig:
1919

2020
dist_name: str | None
2121
pyproject_data: PyProjectData | None
22-
overrides: dict[str, Any]
22+
overrides: dict[str, Any] | None
23+
24+
def apply(self, dist: Any) -> None:
25+
"""Apply version inference to the distribution."""
26+
from .. import _config as _config_module
27+
from .._get_version_impl import _get_version
28+
from .._get_version_impl import _version_missing
29+
30+
# Clear version if it was set by infer_version (overrides is None means infer_version context)
31+
# OR if we have overrides (version_keyword context) and the version was set by infer_version
32+
was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False)
33+
if was_set_by_infer and (self.overrides is None or self.overrides):
34+
dist._setuptools_scm_version_set_by_infer = False
35+
dist.metadata.version = None
36+
37+
config = _config_module.Configuration.from_file(
38+
dist_name=self.dist_name,
39+
pyproject_data=self.pyproject_data,
40+
missing_file_ok=True,
41+
missing_section_ok=True,
42+
**(self.overrides or {}),
43+
)
44+
45+
# Get and assign version
46+
maybe_version = _get_version(config, force_write_version_files=True)
47+
if maybe_version is None:
48+
_version_missing(config)
49+
else:
50+
assert dist.metadata.version is None
51+
dist.metadata.version = maybe_version
52+
53+
# Mark that this version was set by infer_version if overrides is None (infer_version context)
54+
if self.overrides is None:
55+
dist._setuptools_scm_version_set_by_infer = True
2356

2457

2558
@dataclass
@@ -29,19 +62,37 @@ class VersionInferenceError:
2962
message: str
3063
should_warn: bool = False
3164

65+
def apply(self, dist: Any) -> None:
66+
"""Apply error handling to the distribution."""
67+
import warnings
68+
69+
if self.should_warn:
70+
warnings.warn(self.message)
71+
3272

3373
@dataclass
3474
class VersionInferenceException:
3575
"""Exception that should be raised."""
3676

3777
exception: Exception
3878

79+
def apply(self, dist: Any) -> None:
80+
"""Apply exception handling to the distribution."""
81+
raise self.exception
82+
83+
84+
class VersionInferenceNoOp:
85+
"""No operation result - silent skip."""
86+
87+
def apply(self, dist: Any) -> None:
88+
"""Apply no-op to the distribution."""
89+
3990

4091
VersionInferenceResult = Union[
4192
VersionInferenceConfig, # Proceed with inference
4293
VersionInferenceError, # Show error/warning
4394
VersionInferenceException, # Raise exception
44-
None, # Don't infer (silent)
95+
VersionInferenceNoOp, # Don't infer (silent)
4596
]
4697

4798

@@ -71,16 +122,26 @@ def get_version_inference_config(
71122
# Handle version already set
72123
if current_version is not None:
73124
if was_set_by_infer:
74-
if overrides is not None:
75-
# Clear version and proceed with overrides
125+
if overrides is not None and overrides:
126+
# Clear version and proceed with actual overrides (non-empty dict)
76127
return VersionInferenceConfig(
77128
dist_name=dist_name,
78129
pyproject_data=pyproject_data,
79130
overrides=overrides,
80131
)
81132
else:
82-
# Keep existing version from infer_version
83-
return None
133+
# Keep existing version from infer_version (no overrides or empty overrides)
134+
# But allow re-inferring if this is another infer_version call
135+
if overrides is None:
136+
# This is another infer_version call, allow it to proceed
137+
return VersionInferenceConfig(
138+
dist_name=dist_name,
139+
pyproject_data=pyproject_data,
140+
overrides=overrides,
141+
)
142+
else:
143+
# This is version_keyword with empty overrides, keep existing version
144+
return VersionInferenceNoOp()
84145
else:
85146
# Version set by something else
86147
return VersionInferenceError(
@@ -89,7 +150,7 @@ def get_version_inference_config(
89150

90151
# Handle setuptools-scm package
91152
if dist_name == "setuptools-scm":
92-
return None
153+
return VersionInferenceNoOp()
93154

94155
# Handle missing configuration
95156
if not pyproject_data.is_required and not pyproject_data.section_present:
@@ -100,19 +161,19 @@ def get_version_inference_config(
100161
pyproject_data=pyproject_data,
101162
overrides=overrides,
102163
)
103-
return None
164+
return VersionInferenceNoOp()
104165

105166
# Handle missing project section when required
106167
if (
107168
pyproject_data.is_required
108169
and not pyproject_data.section_present
109170
and not pyproject_data.project_present
110171
):
111-
return None
172+
return VersionInferenceNoOp()
112173

113174
# All conditions met - proceed with inference
114175
return VersionInferenceConfig(
115176
dist_name=dist_name,
116177
pyproject_data=pyproject_data,
117-
overrides=overrides or {},
178+
overrides=overrides,
118179
)

testing/test_integration.py

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -656,12 +656,12 @@ def test_setuptools_version_keyword_ensures_regex(
656656
wd.commit_testfile("test")
657657
wd("git tag 1.0")
658658
monkeypatch.chdir(wd.cwd)
659-
import setuptools
660659

661660
from setuptools_scm._integration.setuptools import version_keyword
662661

663-
dist = setuptools.Distribution({"name": "test"})
662+
dist = create_clean_distribution("test")
664663
version_keyword(dist, "use_scm_version", {"tag_regex": "(1.0)"})
664+
assert dist.metadata.version == "1.0"
665665

666666

667667
@pytest.mark.parametrize(
@@ -917,6 +917,27 @@ def test_improved_error_message_mentions_both_config_options(
917917
assert "requires" in error_msg
918918

919919

920+
# Helper function for creating and managing distribution objects
921+
def create_clean_distribution(name: str) -> setuptools.Distribution:
922+
"""Create a clean distribution object without any setuptools_scm effects.
923+
924+
This function creates a new setuptools Distribution and ensures it's completely
925+
clean from any previous setuptools_scm version inference effects, including:
926+
- Clearing any existing version
927+
- Removing the _setuptools_scm_version_set_by_infer flag
928+
"""
929+
import setuptools
930+
931+
dist = setuptools.Distribution({"name": name})
932+
933+
# Clean all setuptools_scm effects
934+
dist.metadata.version = None
935+
if hasattr(dist, "_setuptools_scm_version_set_by_infer"):
936+
delattr(dist, "_setuptools_scm_version_set_by_infer")
937+
938+
return dist
939+
940+
920941
# Helper functions for testing integration point ordering
921942
def integration_infer_version(dist: setuptools.Distribution) -> str:
922943
"""Helper to call infer_version and return the result."""
@@ -1053,12 +1074,10 @@ def test_infer_version_with_build_requires_no_tool_section(
10531074
"""
10541075
wd.write("pyproject.toml", pyproject_content)
10551076

1056-
import setuptools
1057-
10581077
from setuptools_scm._integration.setuptools import infer_version
10591078

1060-
# Create distribution
1061-
dist = setuptools.Distribution({"name": "test-package-infer-version"})
1079+
# Create clean distribution
1080+
dist = create_clean_distribution("test-package-infer-version")
10621081

10631082
# Call infer_version - this should work because setuptools_scm is in build-system.requires
10641083
infer_version(dist)
@@ -1095,12 +1114,10 @@ def test_infer_version_with_build_requires_dash_variant_no_tool_section(
10951114
"""
10961115
wd.write("pyproject.toml", pyproject_content)
10971116

1098-
import setuptools
1099-
11001117
from setuptools_scm._integration.setuptools import infer_version
11011118

1102-
# Create distribution
1103-
dist = setuptools.Distribution({"name": "test-package-infer-version-dash"})
1119+
# Create clean distribution
1120+
dist = create_clean_distribution("test-package-infer-version-dash")
11041121

11051122
# Call infer_version - this should work because setuptools-scm is in build-system.requires
11061123
infer_version(dist)
@@ -1137,12 +1154,10 @@ def test_infer_version_without_build_requires_no_tool_section_silently_returns(
11371154
"""
11381155
wd.write("pyproject.toml", pyproject_content)
11391156

1140-
import setuptools
1141-
11421157
from setuptools_scm._integration.setuptools import infer_version
11431158

1144-
# Create distribution
1145-
dist = setuptools.Distribution({"name": "test-package-no-scm"})
1159+
# Create clean distribution
1160+
dist = create_clean_distribution("test-package-no-scm")
11461161

11471162
infer_version(dist)
11481163
assert dist.metadata.version is None
@@ -1297,12 +1312,10 @@ def test_infer_version_logs_debug_when_missing_dynamic_version(
12971312
"""
12981313
wd.write("pyproject.toml", pyproject_content)
12991314

1300-
import setuptools
1301-
13021315
from setuptools_scm._integration.setuptools import infer_version
13031316

1304-
# Create distribution
1305-
dist = setuptools.Distribution({"name": "test-package-missing-dynamic"})
1317+
# Create clean distribution
1318+
dist = create_clean_distribution("test-package-missing-dynamic")
13061319

13071320
# This should not raise an error, but should log debug info about the configuration issue
13081321
infer_version(dist)

0 commit comments

Comments
 (0)