Skip to content

Commit a0aefa2

Browse files
Merge pull request #947 from RonnyPfannschmidt/fix-938-self-reinit-fixup
correctly handle project config overrides when the version keyword is used together with pyproject.toml
2 parents dc96f83 + 8d3560e commit a0aefa2

13 files changed

+173
-93
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
### Changed
3+
4+
- ensure the setuptools version keyword correctly load pyproject.toml configuration
5+
- add build and wheel to the test requirements for regression testing
6+
- move internal toml handling to own module

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ rich = [
5757
"rich",
5858
]
5959
test = [
60+
"build",
6061
"pytest",
6162
"rich",
63+
"wheel",
6264
]
6365
toml = [
6466
]

src/setuptools_scm/_config.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import os
66
import re
77
import warnings
8+
from pathlib import Path
89
from typing import Any
9-
from typing import Callable
1010
from typing import Pattern
1111
from typing import Protocol
1212

@@ -114,7 +114,7 @@ def from_file(
114114
cls,
115115
name: str | os.PathLike[str] = "pyproject.toml",
116116
dist_name: str | None = None,
117-
_load_toml: Callable[[str], dict[str, Any]] | None = None,
117+
_require_section: bool = True,
118118
**kwargs: Any,
119119
) -> Configuration:
120120
"""
@@ -124,11 +124,12 @@ def from_file(
124124
not contain the [tool.setuptools_scm] section.
125125
"""
126126

127-
pyproject_data = _read_pyproject(name, _load_toml=_load_toml)
127+
pyproject_data = _read_pyproject(Path(name), require_section=_require_section)
128128
args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs)
129129

130130
args.update(read_toml_overrides(args["dist_name"]))
131-
return cls.from_data(relative_to=name, data=args)
131+
relative_to = args.pop("relative_to", name)
132+
return cls.from_data(relative_to=relative_to, data=args)
132133

133134
@classmethod
134135
def from_data(

src/setuptools_scm/_entrypoints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def entry_points(group: str) -> EntryPoints:
4545

4646

4747
def version_from_entrypoint(
48-
config: Configuration, entrypoint: str, root: _t.PathT
48+
config: Configuration, *, entrypoint: str, root: _t.PathT
4949
) -> version.ScmVersion | None:
5050
from .discover import iter_matching_entrypoints
5151

src/setuptools_scm/_get_version_impl.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,22 @@ def parse_scm_version(config: Configuration) -> ScmVersion | None:
3232
)
3333
return parse_result
3434
else:
35-
entrypoint = "setuptools_scm.parse_scm"
36-
root = config.absolute_root
37-
return _entrypoints.version_from_entrypoint(config, entrypoint, root)
35+
return _entrypoints.version_from_entrypoint(
36+
config,
37+
entrypoint="setuptools_scm.parse_scm",
38+
root=config.absolute_root,
39+
)
3840
except _run_cmd.CommandNotFoundError as e:
3941
_log.exception("command %s not found while parsing the scm, using fallbacks", e)
4042
return None
4143

4244

4345
def parse_fallback_version(config: Configuration) -> ScmVersion | None:
44-
entrypoint = "setuptools_scm.parse_scm_fallback"
45-
root = config.fallback_root
46-
return _entrypoints.version_from_entrypoint(config, entrypoint, root)
46+
return _entrypoints.version_from_entrypoint(
47+
config,
48+
entrypoint="setuptools_scm.parse_scm_fallback",
49+
root=config.fallback_root,
50+
)
4751

4852

4953
def parse_version(config: Configuration) -> ScmVersion | None:
Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
11
from __future__ import annotations
22

3-
import os
4-
import sys
53
import warnings
6-
from typing import Any
7-
from typing import Callable
8-
from typing import Dict
4+
from pathlib import Path
95
from typing import NamedTuple
10-
from typing import TYPE_CHECKING
116

7+
from .. import _log
128
from .setuptools import read_dist_name_from_setup_cfg
9+
from .toml import read_toml_content
10+
from .toml import TOML_RESULT
1311

14-
if TYPE_CHECKING:
15-
from typing_extensions import TypeAlias
12+
13+
log = _log.log.getChild("pyproject_reading")
1614

1715
_ROOT = "root"
18-
TOML_RESULT: TypeAlias = Dict[str, Any]
19-
TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT]
2016

2117

2218
class PyProjectData(NamedTuple):
23-
name: str | os.PathLike[str]
19+
path: Path
2420
tool_name: str
2521
project: TOML_RESULT
2622
section: TOML_RESULT
@@ -30,31 +26,24 @@ def project_name(self) -> str | None:
3026
return self.project.get("name")
3127

3228

33-
def lazy_toml_load(data: str) -> TOML_RESULT:
34-
if sys.version_info >= (3, 11):
35-
from tomllib import loads
36-
else:
37-
from tomli import loads
38-
39-
return loads(data)
40-
41-
4229
def read_pyproject(
43-
name: str | os.PathLike[str] = "pyproject.toml",
30+
path: Path = Path("pyproject.toml"),
4431
tool_name: str = "setuptools_scm",
45-
_load_toml: TOML_LOADER | None = None,
32+
require_section: bool = True,
4633
) -> PyProjectData:
47-
if _load_toml is None:
48-
_load_toml = lazy_toml_load
49-
with open(name, encoding="UTF-8") as strm:
50-
data = strm.read()
51-
defn = _load_toml(data)
34+
defn = read_toml_content(path, None if require_section else {})
5235
try:
5336
section = defn.get("tool", {})[tool_name]
5437
except LookupError as e:
55-
raise LookupError(f"{name} does not contain a tool.{tool_name} section") from e
38+
error = f"{path} does not contain a tool.{tool_name} section"
39+
if require_section:
40+
raise LookupError(error) from e
41+
else:
42+
log.warning("toml section missing %r", error)
43+
section = {}
44+
5645
project = defn.get("project", {})
57-
return PyProjectData(name, tool_name, project, section)
46+
return PyProjectData(path, tool_name, project, section)
5847

5948

6049
def get_args_for_pyproject(
@@ -68,7 +57,7 @@ def get_args_for_pyproject(
6857
if "relative_to" in section:
6958
relative = section.pop("relative_to")
7059
warnings.warn(
71-
f"{pyproject.name}: at [tool.{pyproject.tool_name}]\n"
60+
f"{pyproject.path}: at [tool.{pyproject.tool_name}]\n"
7261
f"ignoring value relative_to={relative!r}"
7362
" as its always relative to the config file"
7463
)
@@ -92,5 +81,5 @@ def get_args_for_pyproject(
9281
f"root {section[_ROOT]} is overridden"
9382
f" by the cli arg {kwargs[_ROOT]}"
9483
)
95-
section.pop("root", None)
84+
section.pop(_ROOT, None)
9685
return {"dist_name": dist_name, **section, **kwargs}

src/setuptools_scm/_integration/setuptools.py

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import setuptools
1010

1111
from .. import _config
12-
from .._version_cls import _validate_version_cls
1312

1413
log = logging.getLogger(__name__)
1514

@@ -63,50 +62,47 @@ def _assign_version(
6362
_warn_on_old_setuptools()
6463

6564

65+
def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None:
66+
log.debug("%s %r", hook, vars(dist.metadata))
67+
68+
6669
def version_keyword(
6770
dist: setuptools.Distribution,
6871
keyword: str,
6972
value: bool | dict[str, Any] | Callable[[], dict[str, Any]],
7073
) -> None:
71-
if not value:
72-
return
73-
elif value is True:
74-
value = {}
74+
overrides: dict[str, Any]
75+
if value is True:
76+
overrides = {}
7577
elif callable(value):
76-
value = value()
78+
overrides = value()
79+
else:
80+
assert isinstance(value, dict), "version_keyword expects a dict or True"
81+
overrides = value
82+
7783
assert (
78-
"dist_name" not in value
84+
"dist_name" not in overrides
7985
), "dist_name may not be specified in the setup keyword "
8086
dist_name: str | None = dist.metadata.name
87+
_log_hookstart("version_keyword", dist)
88+
8189
if dist.metadata.version is not None:
8290
warnings.warn(f"version of {dist_name} already set")
8391
return
84-
log.debug(
85-
"version keyword %r",
86-
vars(dist.metadata),
87-
)
88-
log.debug("dist %s %s", id(dist), id(dist.metadata))
8992

9093
if dist_name is None:
9194
dist_name = read_dist_name_from_setup_cfg()
92-
version_cls = value.pop("version_cls", None)
93-
normalize = value.pop("normalize", True)
94-
tag_regex = _config._check_tag_regex(
95-
value.pop("tag_regex", _config.DEFAULT_TAG_REGEX)
96-
)
97-
final_version = _validate_version_cls(version_cls, normalize)
9895

99-
config = _config.Configuration(
100-
dist_name=dist_name, version_cls=final_version, tag_regex=tag_regex, **value
96+
config = _config.Configuration.from_file(
97+
dist_name=dist_name,
98+
_require_section=False,
99+
**overrides,
101100
)
102101
_assign_version(dist, config)
103102

104103

105104
def infer_version(dist: setuptools.Distribution) -> None:
106-
log.debug(
107-
"finalize hook %r",
108-
vars(dist.metadata),
109-
)
105+
_log_hookstart("infer_version", dist)
110106
log.debug("dist %s %s", id(dist), id(dist.metadata))
111107
if dist.metadata.version is not None:
112108
return # metadata already added by hook
@@ -120,6 +116,6 @@ def infer_version(dist: setuptools.Distribution) -> None:
120116
try:
121117
config = _config.Configuration.from_file(dist_name=dist_name)
122118
except LookupError as e:
123-
log.exception(e)
119+
log.warning(e)
124120
else:
125121
_assign_version(dist, config)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
from pathlib import Path
5+
from typing import Any
6+
from typing import Callable
7+
from typing import cast
8+
from typing import Dict
9+
from typing import TYPE_CHECKING
10+
from typing import TypedDict
11+
12+
if sys.version_info >= (3, 11):
13+
from tomllib import loads as load_toml
14+
else:
15+
from tomli import loads as load_toml
16+
17+
if TYPE_CHECKING:
18+
from typing_extensions import TypeAlias
19+
20+
from .. import _log
21+
22+
log = _log.log.getChild("toml")
23+
24+
TOML_RESULT: TypeAlias = Dict[str, Any]
25+
TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT]
26+
27+
28+
def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RESULT:
29+
try:
30+
data = path.read_text(encoding="utf-8")
31+
except FileNotFoundError:
32+
if default is None:
33+
raise
34+
else:
35+
log.debug("%s missing, presuming default %r", path, default)
36+
return default
37+
else:
38+
return load_toml(data)
39+
40+
41+
class _CheatTomlData(TypedDict):
42+
cheat: dict[str, Any]
43+
44+
45+
def load_toml_or_inline_map(data: str | None) -> dict[str, Any]:
46+
"""
47+
load toml data - with a special hack if only a inline map is given
48+
"""
49+
if not data:
50+
return {}
51+
elif data[0] == "{":
52+
data = "cheat=" + data
53+
loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data))
54+
return loaded["cheat"]
55+
return load_toml(data)

src/setuptools_scm/_overrides.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
import os
44
import re
55
from typing import Any
6-
from typing import cast
7-
from typing import TypedDict
86

97
from . import _config
108
from . import _log
119
from . import version
12-
from ._integration.pyproject_reading import lazy_toml_load
10+
from ._integration.toml import load_toml_or_inline_map
1311

1412
log = _log.log.getChild("overrides")
1513

@@ -51,23 +49,6 @@ def _read_pretended_version_for(
5149
return None
5250

5351

54-
class _CheatTomlData(TypedDict):
55-
cheat: dict[str, Any]
56-
57-
58-
def load_toml_or_inline_map(data: str | None) -> dict[str, Any]:
59-
"""
60-
load toml data - with a special hack if only a inline map is given
61-
"""
62-
if not data:
63-
return {}
64-
elif data[0] == "{":
65-
data = "cheat=" + data
66-
loaded: _CheatTomlData = cast(_CheatTomlData, lazy_toml_load(data))
67-
return loaded["cheat"]
68-
return lazy_toml_load(data)
69-
70-
7152
def read_toml_overrides(dist_name: str | None) -> dict[str, Any]:
7253
data = read_named_env(name="OVERRIDES", dist_name=dist_name)
7354
return load_toml_or_inline_map(data)

src/setuptools_scm/_version_cls.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
from logging import getLogger
43
from typing import cast
54
from typing import Type
65
from typing import Union
@@ -11,6 +10,9 @@
1110
except ImportError:
1211
from setuptools.extern.packaging.version import InvalidVersion # type: ignore
1312
from setuptools.extern.packaging.version import Version as Version # type: ignore
13+
from . import _log
14+
15+
log = _log.log.getChild("version_cls")
1416

1517

1618
class NonNormalizedVersion(Version):
@@ -41,10 +43,8 @@ def __repr__(self) -> str:
4143
def _version_as_tuple(version_str: str) -> tuple[int | str, ...]:
4244
try:
4345
parsed_version = Version(version_str)
44-
except InvalidVersion:
45-
log = getLogger(__name__).parent
46-
assert log is not None
47-
log.error("failed to parse version %s", version_str)
46+
except InvalidVersion as e:
47+
log.error("failed to parse version %s: %s", e, version_str)
4848
return (version_str,)
4949
else:
5050
version_fields: tuple[int | str, ...] = parsed_version.release

0 commit comments

Comments
 (0)