Skip to content

Commit

Permalink
Merge pull request #736 from RonnyPfannschmidt/allow-cli-root-absolute
Browse files Browse the repository at this point in the history
cleanup pyproject loading and allow cli relative roots to be specified
  • Loading branch information
RonnyPfannschmidt authored Jun 28, 2022
2 parents 1ebac97 + 27b096e commit 775b9a0
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 72 deletions.
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+")

0 comments on commit 775b9a0

Please sign in to comment.