Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cleanup pyproject loading and allow cli relative roots to be specified #736

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ v7.0.4

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

v7.0.3
=======
Expand Down
5 changes: 4 additions & 1 deletion src/setuptools_scm/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ def main(args: list[str] | None = None) -> None:

try:

config = Configuration.from_file(pyproject, root=opts.root)
config = Configuration.from_file(
pyproject,
root=(os.path.abspath(opts.root) if opts.root is not None else None),
)
except (LookupError, FileNotFoundError) as ex:
# no pyproject.toml OR no [tool.setuptools_scm]
print(
Expand Down
Empty file.
84 changes: 84 additions & 0 deletions src/setuptools_scm/_integration/pyproject_reading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from __future__ import annotations

import warnings
from typing import Any
from typing import Callable
from typing import Dict
from typing import NamedTuple
from typing import TYPE_CHECKING

from .setuptools import read_dist_name_from_setup_cfg

if TYPE_CHECKING:
from typing_extensions import TypeAlias

_ROOT = "root"
TOML_RESULT: TypeAlias = Dict[str, Any]
TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT]


class PyProjectData(NamedTuple):
tool_name: str
project: TOML_RESULT
section: TOML_RESULT

@property
def project_name(self) -> str | None:
return self.project.get("name")


def lazy_tomli_load(data: str) -> TOML_RESULT:
from tomli import loads

return loads(data)


def read_pyproject(
name: str = "pyproject.toml",
tool_name: str = "setuptools_scm",
_load_toml: TOML_LOADER | None = None,
) -> PyProjectData:
if _load_toml is None:
_load_toml = lazy_tomli_load
with open(name, encoding="UTF-8") as strm:
data = strm.read()
defn = _load_toml(data)
try:
section = defn.get("tool", {})[tool_name]
except LookupError as e:
raise LookupError(f"{name} does not contain a tool.{tool_name} section") from e
project = defn.get("project", {})
return PyProjectData(tool_name, project, section)


def get_args_for_pyproject(
pyproject: PyProjectData,
dist_name: str | None,
kwargs: TOML_RESULT,
) -> TOML_RESULT:
"""drops problematic details and figures the distribution name"""
section = pyproject.section.copy()
kwargs = kwargs.copy()

if "dist_name" in section:
if dist_name is None:
dist_name = section.pop("dist_name")
else:
assert dist_name == section["dist_name"]
del section["dist_name"]
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)
elif _ROOT in section:
if section[_ROOT] != kwargs[_ROOT]:
warnings.warn(
f"root {section[_ROOT]} is overridden"
f" by the cli arg {kwargs[_ROOT]}"
)
section.pop("root", None)
return {"dist_name": dist_name, **section, **kwargs}
22 changes: 22 additions & 0 deletions src/setuptools_scm/_integration/setuptools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

import os
from typing import IO


def read_dist_name_from_setup_cfg(
input: str | os.PathLike[str] | IO[str] = "setup.cfg",
) -> str | None:

# minimal effort to read dist_name off setup.cfg metadata
import configparser

parser = configparser.ConfigParser()

if isinstance(input, (os.PathLike, str)):
parser.read([input])
else:
parser.read_file(input)

dist_name = parser.get("metadata", "name", fallback=None)
return dist_name
76 changes: 10 additions & 66 deletions src/setuptools_scm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
from typing import TYPE_CHECKING
from typing import Union

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


def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
Expand All @@ -47,7 +50,8 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
if relative_to:
if (
os.path.isabs(root)
and not os.path.commonpath([root, relative_to]) == relative_to
and os.path.isabs(relative_to)
and not os.path.commonpath([root, relative_to]) == root
):
warnings.warn(
"absolute root path '%s' overrides relative_to '%s'"
Expand All @@ -67,12 +71,6 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
return os.path.abspath(root)


def _lazy_tomli_load(data: str) -> dict[str, Any]:
from tomli import loads

return loads(data)


_VersionT = Union[Version, NonNormalizedVersion]


Expand Down Expand Up @@ -202,7 +200,7 @@ def from_file(
cls,
name: str = "pyproject.toml",
dist_name: str | None = None,
_load_toml: Callable[[str], dict[str, Any]] = _lazy_tomli_load,
_load_toml: Callable[[str], dict[str, Any]] | None = None,
**kwargs: Any,
) -> Configuration:
"""
Expand All @@ -212,61 +210,7 @@ def from_file(
not contain the [tool.setuptools_scm] section.
"""

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

defn = _load_toml(data)
try:
section = defn.get("tool", {})["setuptools_scm"]
except LookupError as e:
raise LookupError(
f"{name} does not contain a tool.setuptools_scm section"
) from e

project = defn.get("project", {})
dist_name = cls._cleanup_from_file_args_data(
project, dist_name, kwargs, section
)
return cls(dist_name=dist_name, relative_to=name, **section, **kwargs)

@staticmethod
def _cleanup_from_file_args_data(
project: dict[str, Any],
dist_name: str | None,
kwargs: dict[str, Any],
section: dict[str, Any],
) -> str | None:
"""drops problematic details and figures the distribution name"""
if "dist_name" in section:
if dist_name is None:
dist_name = section.pop("dist_name")
else:
assert dist_name == section["dist_name"]
del section["dist_name"]
if dist_name is None:
# minimal pep 621 support for figuring the pretend keys
dist_name = project.get("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)
elif _ROOT in section:
if section[_ROOT] != kwargs[_ROOT]:
warnings.warn(
f"root {section[_ROOT]} is overridden"
f" by the cli arg {kwargs[_ROOT]}"
)
section.pop("root", None)
return dist_name


def _read_dist_name_from_setup_cfg() -> str | None:

# minimal effort to read dist_name off setup.cfg metadata
import configparser

parser = configparser.ConfigParser()
parser.read(["setup.cfg"])
dist_name = parser.get("metadata", "name", fallback=None)
return dist_name
return cls(relative_to=name, **args)
4 changes: 3 additions & 1 deletion src/setuptools_scm/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from . import _get_version
from . import _version_missing
from ._entrypoints import iter_entry_points
from .config import _read_dist_name_from_setup_cfg
from ._integration.setuptools import (
read_dist_name_from_setup_cfg as _read_dist_name_from_setup_cfg,
)
from .config import Configuration
from .utils import do
from .utils import trace
Expand Down
18 changes: 14 additions & 4 deletions testing/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ def get_output(args: list[str]) -> str:
return out.getvalue()


warns_cli_root_override = pytest.warns(
UserWarning, match="root .. is overridden by the cli arg ."
)
warns_absolute_root_override = pytest.warns(
UserWarning, match="absolute root path '.*' overrides relative_to '.*'"
)

exits_with_not_found = pytest.raises(SystemExit, match="no version found for")


def test_cli_find_pyproject(
wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode
) -> None:
Expand All @@ -34,17 +44,17 @@ def test_cli_find_pyproject(
out = get_output([])
assert out.startswith("0.1.dev1+")

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

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

with pytest.raises(SystemExit, match="no version found for"):
with exits_with_not_found, warns_absolute_root_override:

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

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