Skip to content

Commit 8c0f121

Browse files
refactor step for version inference - intorduce intermediate types and fix/simplify the tests
1 parent 66e16e2 commit 8c0f121

File tree

9 files changed

+671
-101
lines changed

9 files changed

+671
-101
lines changed

src/setuptools_scm/_config.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -291,24 +291,12 @@ def from_file(
291291
- **kwargs: additional keyword arguments to pass to the Configuration constructor
292292
"""
293293

294-
try:
295-
if pyproject_data is None:
296-
pyproject_data = _read_pyproject(
297-
Path(name), missing_section_ok=missing_section_ok
298-
)
299-
except FileNotFoundError:
300-
if missing_file_ok:
301-
log.warning("File %s not found, using empty configuration", name)
302-
pyproject_data = PyProjectData(
303-
path=Path(name),
304-
tool_name="setuptools_scm",
305-
project={},
306-
section={},
307-
is_required=False,
308-
section_present=False,
309-
)
310-
else:
311-
raise
294+
if pyproject_data is None:
295+
pyproject_data = _read_pyproject(
296+
Path(name),
297+
missing_section_ok=missing_section_ok,
298+
missing_file_ok=missing_file_ok,
299+
)
312300
args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs)
313301

314302
args.update(read_toml_overrides(args["dist_name"]))

src/setuptools_scm/_integration/pyproject_reading.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
import warnings
44

5+
from dataclasses import dataclass
56
from pathlib import Path
6-
from typing import NamedTuple
77
from typing import Sequence
88

99
from .. import _log
10-
from .setuptools import read_dist_name_from_setup_cfg
1110
from .toml import TOML_RESULT
1211
from .toml import read_toml_content
1312

@@ -16,13 +15,39 @@
1615
_ROOT = "root"
1716

1817

19-
class PyProjectData(NamedTuple):
18+
@dataclass
19+
class PyProjectData:
2020
path: Path
2121
tool_name: str
2222
project: TOML_RESULT
2323
section: TOML_RESULT
2424
is_required: bool
2525
section_present: bool
26+
project_present: bool
27+
28+
@classmethod
29+
def for_testing(
30+
cls,
31+
is_required: bool = False,
32+
section_present: bool = False,
33+
project_present: bool = False,
34+
project_name: str | None = None,
35+
) -> PyProjectData:
36+
"""Create a PyProjectData instance for testing purposes."""
37+
if project_name is not None:
38+
project = {"name": project_name}
39+
assert project_present
40+
else:
41+
project = {}
42+
return cls(
43+
path=Path("pyproject.toml"),
44+
tool_name="setuptools_scm",
45+
project=project,
46+
section={},
47+
is_required=is_required,
48+
section_present=section_present,
49+
project_present=project_present,
50+
)
2651

2752
@property
2853
def project_name(self) -> str | None:
@@ -33,6 +58,10 @@ def verify_dynamic_version_when_required(self) -> None:
3358
if self.is_required and not self.section_present:
3459
# When setuptools-scm is in build-system.requires but no tool section exists,
3560
# we need to verify that dynamic=['version'] is set in the project section
61+
# But only if there's actually a project section
62+
if not self.project_present:
63+
# No project section, so don't auto-activate setuptools_scm
64+
return
3665
dynamic = self.project.get("dynamic", [])
3766
if "version" not in dynamic:
3867
raise ValueError(
@@ -62,8 +91,25 @@ def read_pyproject(
6291
tool_name: str = "setuptools_scm",
6392
build_package_names: Sequence[str] = ("setuptools_scm", "setuptools-scm"),
6493
missing_section_ok: bool = False,
94+
missing_file_ok: bool = False,
6595
) -> PyProjectData:
66-
defn = read_toml_content(path)
96+
try:
97+
defn = read_toml_content(path)
98+
except FileNotFoundError:
99+
if missing_file_ok:
100+
log.warning("File %s not found, using empty configuration", path)
101+
return PyProjectData(
102+
path=path,
103+
tool_name=tool_name,
104+
project={},
105+
section={},
106+
is_required=False,
107+
section_present=False,
108+
project_present=False,
109+
)
110+
else:
111+
raise
112+
67113
requires: list[str] = defn.get("build-system", {}).get("requires", [])
68114
is_required = has_build_package(requires, build_package_names)
69115

@@ -87,8 +133,9 @@ def read_pyproject(
87133
section_present = False
88134

89135
project = defn.get("project", {})
136+
project_present = "project" in defn
90137
pyproject_data = PyProjectData(
91-
path, tool_name, project, section, is_required, section_present
138+
path, tool_name, project, section, is_required, section_present, project_present
92139
)
93140

94141
# Verify dynamic version when setuptools-scm is used as build dependency indicator
@@ -121,8 +168,6 @@ def get_args_for_pyproject(
121168
if dist_name is None:
122169
# minimal pep 621 support for figuring the pretend keys
123170
dist_name = pyproject.project_name
124-
if dist_name is None:
125-
dist_name = read_dist_name_from_setup_cfg()
126171
if _ROOT in kwargs:
127172
if kwargs[_ROOT] is None:
128173
kwargs.pop(_ROOT, None)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from __future__ import annotations
2+
3+
import os
4+
5+
import setuptools
6+
7+
8+
def read_dist_name_from_setup_cfg(
9+
input: str | os.PathLike[str] = "setup.cfg",
10+
) -> str | None:
11+
# minimal effort to read dist_name off setup.cfg metadata
12+
import configparser
13+
14+
parser = configparser.ConfigParser()
15+
parser.read([input], encoding="utf-8")
16+
dist_name = parser.get("metadata", "name", fallback=None)
17+
return dist_name
18+
19+
20+
def _dist_name_from_legacy(dist: setuptools.Distribution) -> str | None:
21+
return dist.metadata.name or read_dist_name_from_setup_cfg()
Lines changed: 85 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import logging
4-
import os
54
import warnings
65

76
from pathlib import Path
@@ -11,22 +10,16 @@
1110
import setuptools
1211

1312
from .. import _config
13+
from .pyproject_reading import read_pyproject
14+
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
18+
from .version_inference import get_version_inference_config
1419

1520
log = logging.getLogger(__name__)
1621

1722

18-
def read_dist_name_from_setup_cfg(
19-
input: str | os.PathLike[str] = "setup.cfg",
20-
) -> str | None:
21-
# minimal effort to read dist_name off setup.cfg metadata
22-
import configparser
23-
24-
parser = configparser.ConfigParser()
25-
parser.read([input], encoding="utf-8")
26-
dist_name = parser.get("metadata", "name", fallback=None)
27-
return dist_name
28-
29-
3023
def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None:
3124
if int(_version.split(".")[0]) < 61:
3225
warnings.warn(
@@ -85,14 +78,15 @@ def _assign_version(
8578

8679

8780
def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None:
88-
log.debug("%s %r", hook, vars(dist.metadata))
81+
log.debug("%s %s %s %r", hook, id(dist), id(dist.metadata), vars(dist.metadata))
8982

9083

9184
def version_keyword(
9285
dist: setuptools.Distribution,
9386
keyword: str,
9487
value: bool | dict[str, Any] | Callable[[], dict[str, Any]],
9588
) -> None:
89+
# Parse overrides (integration point responsibility)
9690
overrides: dict[str, Any]
9791
if value is True:
9892
overrides = {}
@@ -105,73 +99,96 @@ def version_keyword(
10599
assert "dist_name" not in overrides, (
106100
"dist_name may not be specified in the setup keyword "
107101
)
108-
dist_name: str | None = dist.metadata.name
102+
103+
dist_name: str | None = _dist_name_from_legacy(dist)
104+
105+
was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False)
109106
_log_hookstart("version_keyword", dist)
110107

111-
if dist.metadata.version is not None:
112-
# Check if version was set by infer_version
113-
was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False)
108+
# Get pyproject data
109+
try:
110+
pyproject_data = read_pyproject(
111+
Path("pyproject.toml"), missing_section_ok=True, missing_file_ok=True
112+
)
113+
except (LookupError, ValueError) as e:
114+
log.debug("Configuration issue in pyproject.toml: %s", e)
115+
return
114116

117+
# Get decision
118+
result = get_version_inference_config(
119+
dist_name=dist_name,
120+
current_version=dist.metadata.version,
121+
pyproject_data=pyproject_data,
122+
overrides=overrides,
123+
was_set_by_infer=was_set_by_infer,
124+
)
125+
126+
# Handle result
127+
if result is None:
128+
return # Don't infer
129+
elif isinstance(result, VersionInferenceError):
130+
if result.should_warn:
131+
warnings.warn(result.message)
132+
return
133+
elif isinstance(result, VersionInferenceException):
134+
raise result.exception
135+
elif isinstance(result, VersionInferenceConfig):
136+
# Clear version if it was set by infer_version
115137
if was_set_by_infer:
116-
# Version was set by infer_version, check if we have overrides
117-
if not overrides:
118-
# No overrides, just use the infer_version result
119-
return
120-
# We have overrides, clear the marker and proceed to override the version
121138
dist._setuptools_scm_version_set_by_infer = False # type: ignore[attr-defined]
122139
dist.metadata.version = None
123-
else:
124-
# Version was set by something else, warn and return
125-
warnings.warn(f"version of {dist_name} already set")
126-
return
127-
128-
if dist_name is None:
129-
dist_name = read_dist_name_from_setup_cfg()
130140

131-
config = _config.Configuration.from_file(
132-
dist_name=dist_name,
133-
missing_file_ok=True,
134-
missing_section_ok=True,
135-
**overrides,
136-
)
137-
_assign_version(dist, config)
141+
# Proceed with inference
142+
config = _config.Configuration.from_file(
143+
dist_name=result.dist_name,
144+
pyproject_data=result.pyproject_data,
145+
missing_file_ok=True,
146+
missing_section_ok=True,
147+
**overrides,
148+
)
149+
_assign_version(dist, config)
138150

139151

140152
def infer_version(dist: setuptools.Distribution) -> None:
141153
_log_hookstart("infer_version", dist)
142-
log.debug("dist %s %s", id(dist), id(dist.metadata))
143-
if dist.metadata.version is not None:
144-
return # metadata already added by hook
145-
dist_name = dist.metadata.name
146-
if dist_name is None:
147-
dist_name = read_dist_name_from_setup_cfg()
148-
if not os.path.isfile("pyproject.toml"):
149-
return
150-
if dist_name == "setuptools-scm":
151-
return
152154

153-
# Check if setuptools-scm is configured before proceeding
154-
try:
155-
from .pyproject_reading import read_pyproject
155+
dist_name = _dist_name_from_legacy(dist)
156156

157+
# Get pyproject data (integration point responsibility)
158+
try:
157159
pyproject_data = read_pyproject(Path("pyproject.toml"), missing_section_ok=True)
158-
# Only proceed if setuptools-scm is either in build_requires or has a tool section
159-
if not pyproject_data.is_required and not pyproject_data.section_present:
160-
return # No setuptools-scm configuration, silently return
161-
except (FileNotFoundError, LookupError):
162-
return # No pyproject.toml or other issues, silently return
163-
except ValueError as e:
164-
# Log the error as debug info instead of raising it
160+
except FileNotFoundError:
161+
log.debug("pyproject.toml not found")
162+
return
163+
except (LookupError, ValueError) as e:
165164
log.debug("Configuration issue in pyproject.toml: %s", e)
166-
return # Configuration issue, silently return
165+
return
167166

168-
try:
169-
config = _config.Configuration.from_file(
170-
dist_name=dist_name, pyproject_data=pyproject_data
171-
)
172-
except LookupError as e:
173-
log.info(e, exc_info=True)
174-
else:
175-
_assign_version(dist, config)
176-
# Mark that this version was set by infer_version
177-
dist._setuptools_scm_version_set_by_infer = True # type: ignore[attr-defined]
167+
# Get decision
168+
result = get_version_inference_config(
169+
dist_name=dist_name,
170+
current_version=dist.metadata.version,
171+
pyproject_data=pyproject_data,
172+
)
173+
174+
# Handle result
175+
if result is None:
176+
return # Don't infer
177+
elif isinstance(result, VersionInferenceError):
178+
if result.should_warn:
179+
log.warning(result.message)
180+
return
181+
elif isinstance(result, VersionInferenceException):
182+
raise result.exception
183+
elif isinstance(result, VersionInferenceConfig):
184+
# Proceed with inference
185+
try:
186+
config = _config.Configuration.from_file(
187+
dist_name=result.dist_name, pyproject_data=result.pyproject_data
188+
)
189+
except LookupError as e:
190+
log.info(e, exc_info=True)
191+
else:
192+
_assign_version(dist, config)
193+
# Mark that this version was set by infer_version
194+
dist._setuptools_scm_version_set_by_infer = True # type: ignore[attr-defined]

0 commit comments

Comments
 (0)