Skip to content

Commit 775b9a0

Browse files
Merge pull request #736 from RonnyPfannschmidt/allow-cli-root-absolute
cleanup pyproject loading and allow cli relative roots to be specified
2 parents 1ebac97 + 27b096e commit 775b9a0

File tree

8 files changed

+139
-72
lines changed

8 files changed

+139
-72
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ v7.0.4
33

44
* fix #727: correctly handle incomplete archivals from setuptools_scm_git_archival
55
* fix #691: correctly handle specifying root in pyproject.toml
6+
* correct root override check condition (to ensure absolute path matching)
7+
* allow root by the cli to be considered relative to the cli (using abspath)
68

79
v7.0.3
810
=======

src/setuptools_scm/_cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ def main(args: list[str] | None = None) -> None:
1818

1919
try:
2020

21-
config = Configuration.from_file(pyproject, root=opts.root)
21+
config = Configuration.from_file(
22+
pyproject,
23+
root=(os.path.abspath(opts.root) if opts.root is not None else None),
24+
)
2225
except (LookupError, FileNotFoundError) as ex:
2326
# no pyproject.toml OR no [tool.setuptools_scm]
2427
print(

src/setuptools_scm/_integration/__init__.py

Whitespace-only changes.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from __future__ import annotations
2+
3+
import warnings
4+
from typing import Any
5+
from typing import Callable
6+
from typing import Dict
7+
from typing import NamedTuple
8+
from typing import TYPE_CHECKING
9+
10+
from .setuptools import read_dist_name_from_setup_cfg
11+
12+
if TYPE_CHECKING:
13+
from typing_extensions import TypeAlias
14+
15+
_ROOT = "root"
16+
TOML_RESULT: TypeAlias = Dict[str, Any]
17+
TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT]
18+
19+
20+
class PyProjectData(NamedTuple):
21+
tool_name: str
22+
project: TOML_RESULT
23+
section: TOML_RESULT
24+
25+
@property
26+
def project_name(self) -> str | None:
27+
return self.project.get("name")
28+
29+
30+
def lazy_tomli_load(data: str) -> TOML_RESULT:
31+
from tomli import loads
32+
33+
return loads(data)
34+
35+
36+
def read_pyproject(
37+
name: str = "pyproject.toml",
38+
tool_name: str = "setuptools_scm",
39+
_load_toml: TOML_LOADER | None = None,
40+
) -> PyProjectData:
41+
if _load_toml is None:
42+
_load_toml = lazy_tomli_load
43+
with open(name, encoding="UTF-8") as strm:
44+
data = strm.read()
45+
defn = _load_toml(data)
46+
try:
47+
section = defn.get("tool", {})[tool_name]
48+
except LookupError as e:
49+
raise LookupError(f"{name} does not contain a tool.{tool_name} section") from e
50+
project = defn.get("project", {})
51+
return PyProjectData(tool_name, project, section)
52+
53+
54+
def get_args_for_pyproject(
55+
pyproject: PyProjectData,
56+
dist_name: str | None,
57+
kwargs: TOML_RESULT,
58+
) -> TOML_RESULT:
59+
"""drops problematic details and figures the distribution name"""
60+
section = pyproject.section.copy()
61+
kwargs = kwargs.copy()
62+
63+
if "dist_name" in section:
64+
if dist_name is None:
65+
dist_name = section.pop("dist_name")
66+
else:
67+
assert dist_name == section["dist_name"]
68+
del section["dist_name"]
69+
if dist_name is None:
70+
# minimal pep 621 support for figuring the pretend keys
71+
dist_name = pyproject.project_name
72+
if dist_name is None:
73+
dist_name = read_dist_name_from_setup_cfg()
74+
if _ROOT in kwargs:
75+
if kwargs[_ROOT] is None:
76+
kwargs.pop(_ROOT, None)
77+
elif _ROOT in section:
78+
if section[_ROOT] != kwargs[_ROOT]:
79+
warnings.warn(
80+
f"root {section[_ROOT]} is overridden"
81+
f" by the cli arg {kwargs[_ROOT]}"
82+
)
83+
section.pop("root", None)
84+
return {"dist_name": dist_name, **section, **kwargs}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from typing import IO
5+
6+
7+
def read_dist_name_from_setup_cfg(
8+
input: str | os.PathLike[str] | IO[str] = "setup.cfg",
9+
) -> str | None:
10+
11+
# minimal effort to read dist_name off setup.cfg metadata
12+
import configparser
13+
14+
parser = configparser.ConfigParser()
15+
16+
if isinstance(input, (os.PathLike, str)):
17+
parser.read([input])
18+
else:
19+
parser.read_file(input)
20+
21+
dist_name = parser.get("metadata", "name", fallback=None)
22+
return dist_name

src/setuptools_scm/config.py

Lines changed: 10 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
from typing import TYPE_CHECKING
1313
from typing import Union
1414

15+
from ._integration.pyproject_reading import (
16+
get_args_for_pyproject as _get_args_for_pyproject,
17+
)
18+
from ._integration.pyproject_reading import read_pyproject as _read_pyproject
1519
from ._version_cls import NonNormalizedVersion
1620
from ._version_cls import Version
1721
from .utils import trace
@@ -24,7 +28,6 @@
2428
DEFAULT_TAG_REGEX = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
2529
DEFAULT_VERSION_SCHEME = "guess-next-dev"
2630
DEFAULT_LOCAL_SCHEME = "node-and-date"
27-
_ROOT = "root"
2831

2932

3033
def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
@@ -47,7 +50,8 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
4750
if relative_to:
4851
if (
4952
os.path.isabs(root)
50-
and not os.path.commonpath([root, relative_to]) == relative_to
53+
and os.path.isabs(relative_to)
54+
and not os.path.commonpath([root, relative_to]) == root
5155
):
5256
warnings.warn(
5357
"absolute root path '%s' overrides relative_to '%s'"
@@ -67,12 +71,6 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
6771
return os.path.abspath(root)
6872

6973

70-
def _lazy_tomli_load(data: str) -> dict[str, Any]:
71-
from tomli import loads
72-
73-
return loads(data)
74-
75-
7674
_VersionT = Union[Version, NonNormalizedVersion]
7775

7876

@@ -202,7 +200,7 @@ def from_file(
202200
cls,
203201
name: str = "pyproject.toml",
204202
dist_name: str | None = None,
205-
_load_toml: Callable[[str], dict[str, Any]] = _lazy_tomli_load,
203+
_load_toml: Callable[[str], dict[str, Any]] | None = None,
206204
**kwargs: Any,
207205
) -> Configuration:
208206
"""
@@ -212,61 +210,7 @@ def from_file(
212210
not contain the [tool.setuptools_scm] section.
213211
"""
214212

215-
with open(name, encoding="UTF-8") as strm:
216-
data = strm.read()
213+
pyproject_data = _read_pyproject(name, _load_toml=_load_toml)
214+
args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs)
217215

218-
defn = _load_toml(data)
219-
try:
220-
section = defn.get("tool", {})["setuptools_scm"]
221-
except LookupError as e:
222-
raise LookupError(
223-
f"{name} does not contain a tool.setuptools_scm section"
224-
) from e
225-
226-
project = defn.get("project", {})
227-
dist_name = cls._cleanup_from_file_args_data(
228-
project, dist_name, kwargs, section
229-
)
230-
return cls(dist_name=dist_name, relative_to=name, **section, **kwargs)
231-
232-
@staticmethod
233-
def _cleanup_from_file_args_data(
234-
project: dict[str, Any],
235-
dist_name: str | None,
236-
kwargs: dict[str, Any],
237-
section: dict[str, Any],
238-
) -> str | None:
239-
"""drops problematic details and figures the distribution name"""
240-
if "dist_name" in section:
241-
if dist_name is None:
242-
dist_name = section.pop("dist_name")
243-
else:
244-
assert dist_name == section["dist_name"]
245-
del section["dist_name"]
246-
if dist_name is None:
247-
# minimal pep 621 support for figuring the pretend keys
248-
dist_name = project.get("name")
249-
if dist_name is None:
250-
dist_name = _read_dist_name_from_setup_cfg()
251-
if _ROOT in kwargs:
252-
if kwargs[_ROOT] is None:
253-
kwargs.pop(_ROOT, None)
254-
elif _ROOT in section:
255-
if section[_ROOT] != kwargs[_ROOT]:
256-
warnings.warn(
257-
f"root {section[_ROOT]} is overridden"
258-
f" by the cli arg {kwargs[_ROOT]}"
259-
)
260-
section.pop("root", None)
261-
return dist_name
262-
263-
264-
def _read_dist_name_from_setup_cfg() -> str | None:
265-
266-
# minimal effort to read dist_name off setup.cfg metadata
267-
import configparser
268-
269-
parser = configparser.ConfigParser()
270-
parser.read(["setup.cfg"])
271-
dist_name = parser.get("metadata", "name", fallback=None)
272-
return dist_name
216+
return cls(relative_to=name, **args)

src/setuptools_scm/integration.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
from . import _get_version
1212
from . import _version_missing
1313
from ._entrypoints import iter_entry_points
14-
from .config import _read_dist_name_from_setup_cfg
14+
from ._integration.setuptools import (
15+
read_dist_name_from_setup_cfg as _read_dist_name_from_setup_cfg,
16+
)
1517
from .config import Configuration
1618
from .utils import do
1719
from .utils import trace

testing/test_cli.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ def get_output(args: list[str]) -> str:
2323
return out.getvalue()
2424

2525

26+
warns_cli_root_override = pytest.warns(
27+
UserWarning, match="root .. is overridden by the cli arg ."
28+
)
29+
warns_absolute_root_override = pytest.warns(
30+
UserWarning, match="absolute root path '.*' overrides relative_to '.*'"
31+
)
32+
33+
exits_with_not_found = pytest.raises(SystemExit, match="no version found for")
34+
35+
2636
def test_cli_find_pyproject(
2737
wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode
2838
) -> None:
@@ -34,17 +44,17 @@ def test_cli_find_pyproject(
3444
out = get_output([])
3545
assert out.startswith("0.1.dev1+")
3646

37-
with pytest.raises(SystemExit, match="no version found for"):
47+
with exits_with_not_found:
3848
get_output(["--root=.."])
3949

4050
wd.write(PYPROJECT_TOML, PYPROJECT_ROOT)
41-
with pytest.raises(SystemExit, match="no version found for"):
51+
with exits_with_not_found:
4252
print(get_output(["-c", PYPROJECT_TOML]))
4353

44-
with pytest.raises(SystemExit, match="no version found for"):
54+
with exits_with_not_found, warns_absolute_root_override:
4555

4656
get_output(["-c", PYPROJECT_TOML, "--root=.."])
4757

48-
with pytest.warns(UserWarning, match="root .. is overridden by the cli arg ."):
58+
with warns_cli_root_override:
4959
out = get_output(["-c", PYPROJECT_TOML, "--root=."])
5060
assert out.startswith("0.1.dev1+")

0 commit comments

Comments
 (0)