summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRonny Pfannschmidt <opensource@ronnypfannschmidt.de>2023-04-26 22:34:18 +0200
committerGitHub <noreply@github.com>2023-04-26 22:34:18 +0200
commit126a49c3f9e6b4f3f077f83595af08a74895b4a2 (patch)
tree69fc9699ac2987e935ffa6c9c9fa55fb214d5bc3
parent51b3566170be25582b5c3216a54b024caf3d431f (diff)
parent9e9836224b02a9db1a668db8652cb08657a8a9a3 (diff)
downloadsetuptools-scm-126a49c3f9e6b4f3f077f83595af08a74895b4a2.tar.gz
Merge pull request #823 from RonnyPfannschmidt/cleanups
assorted cleanups
-rw-r--r--.github/workflows/python-tests.yml15
-rw-r--r--.pre-commit-config.yaml1
-rw-r--r--CHANGELOG.rst10
-rw-r--r--_own_version_helper.py30
-rw-r--r--pyproject.toml14
-rw-r--r--src/setuptools_scm/__init__.py5
-rw-r--r--src/setuptools_scm/_config.py10
-rw-r--r--src/setuptools_scm/_entrypoints.py79
-rw-r--r--src/setuptools_scm/_file_finders/__init__.py10
-rw-r--r--src/setuptools_scm/_file_finders/git.py9
-rw-r--r--src/setuptools_scm/_file_finders/hg.py8
-rw-r--r--src/setuptools_scm/_log.py94
-rw-r--r--src/setuptools_scm/_overrides.py6
-rw-r--r--src/setuptools_scm/_run_cmd.py116
-rw-r--r--src/setuptools_scm/_trace.py30
-rw-r--r--src/setuptools_scm/_types.py29
-rw-r--r--src/setuptools_scm/_version_cls.py5
-rw-r--r--src/setuptools_scm/discover.py32
-rw-r--r--src/setuptools_scm/fallbacks.py (renamed from src/setuptools_scm/hacks.py)24
-rw-r--r--src/setuptools_scm/git.py252
-rw-r--r--src/setuptools_scm/hg.py41
-rw-r--r--src/setuptools_scm/hg_git.py90
-rw-r--r--src/setuptools_scm/integration.py30
-rw-r--r--src/setuptools_scm/scm_workdir.py27
-rw-r--r--src/setuptools_scm/utils.py79
-rw-r--r--src/setuptools_scm/version.py62
-rw-r--r--testing/conftest.py36
-rw-r--r--testing/test_basic_api.py13
-rw-r--r--testing/test_functions.py47
-rw-r--r--testing/test_git.py68
-rw-r--r--testing/test_hg_git.py44
-rw-r--r--testing/test_integration.py4
-rw-r--r--testing/test_mercurial.py34
-rw-r--r--testing/test_regressions.py25
-rw-r--r--testing/wd_wrapper.py18
-rw-r--r--tox.ini4
36 files changed, 739 insertions, 662 deletions
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index a6fc97f..f0029c9 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -90,21 +90,10 @@ jobs:
- run: |
$(hg debuginstall --template "{pythonexe}") -m pip install hg-git --user
if: matrix.os == 'ubuntu-latest'
+ # this hopefull helps with os caches, hg init sometimes gets 20s timeouts
+ - run: hg version
- run: pytest
- test_legacy_setuptools:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - name: Setup python
- uses: actions/setup-python@v4
- with:
- python-version: "3.8"
- architecture: x64
- - run: pip install -e .[toml,test] pytest virtualenv
- - run: pytest --test-legacy testing/test_setuptools_support.py || true # ignore fail flaky on ci
-
-
dist_upload:
runs-on: ubuntu-latest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6541a0c..6a36020 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -44,3 +44,4 @@ repos:
- pytest == 7.1
- importlib_metadata
- typing-extensions>=4.5
+ - rich
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 71e528a..72efc1c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -8,7 +8,12 @@ breaking
* turn Configuration into a dataclass
* require configuration to always pass into helpers
* hide file-finders implementation in private module
-* define own build backend to have setuptools work
+* migrate to hatchling
+* renamed setuptools_scm.hacks to setuptools_scm.fallbacks and drop support for pip-egg-info
+* remove trace function and use logging instead
+* unify distance=None and distance=0 they should mean the same
+ and where hiding dirty states that are now explicitly dirty
+* depend on later importlib for the full selectable api
features
--------
@@ -27,6 +32,9 @@ features
* pre-compiled regex
* move helpers to private modules
+* support passing log levels to SETUPTOOLS_SCM_DEBUG
+* support using rich.logging as console log handler if installed
+
v7.1.0
======
diff --git a/_own_version_helper.py b/_own_version_helper.py
index b7069ba..e40cb63 100644
--- a/_own_version_helper.py
+++ b/_own_version_helper.py
@@ -4,21 +4,41 @@ to use the attribute for the versions
"""
from __future__ import annotations
+import logging
+from typing import Callable
+
+from setuptools_scm import _types as _t
from setuptools_scm import Configuration
from setuptools_scm import get_version
from setuptools_scm import git
from setuptools_scm import hg
-from setuptools_scm.hacks import parse_pkginfo
+from setuptools_scm.fallbacks import parse_pkginfo
from setuptools_scm.version import get_local_node_and_date
from setuptools_scm.version import guess_next_dev_version
from setuptools_scm.version import ScmVersion
+log = logging.getLogger("setuptools_scm")
+# todo: take fake entrypoints from pyproject.toml
+try_parse: list[Callable[[_t.PathT, Configuration], ScmVersion | None]] = [
+ parse_pkginfo,
+ git.parse,
+ hg.parse,
+ git.parse_archival,
+ hg.parse_archival,
+]
+
def parse(root: str, config: Configuration) -> ScmVersion | None:
- try:
- return parse_pkginfo(root, config)
- except OSError:
- return git.parse(root, config=config) or hg.parse(root, config=config)
+ for maybe_parse in try_parse:
+ try:
+ parsed = maybe_parse(root, config)
+ except OSError as e:
+ log.warning("parse with %s failed with: %s", maybe_parse, e)
+ else:
+ if parsed is not None:
+ return parsed
+ else:
+ return None
def scm_version() -> str:
diff --git a/pyproject.toml b/pyproject.toml
index 1ae670b..86a7402 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,6 +4,7 @@
build-backend = "hatchling.build"
requires = [
"hatchling>=1.10",
+ 'importlib-metadata>=4.6; python_version < "3.10"',
"packaging>=20",
"setuptools>=55",
'tomli; python_version < "3.11"',
@@ -40,15 +41,19 @@ dynamic = [
"version",
]
dependencies = [
- 'importlib-metadata; python_version < "3.8"',
+ 'importlib-metadata>=4.6; python_version < "3.10"',
"packaging>=20",
"setuptools",
'tomli>=1; python_version < "3.11"',
"typing-extensions",
]
[project.optional-dependencies]
+rich = [
+ "rich",
+]
test = [
"pytest",
+ "rich",
"virtualenv>20",
]
toml = [
@@ -78,10 +83,9 @@ node-and-timestamp = "setuptools_scm.version:get_local_node_and_timestamp"
[project.entry-points."setuptools_scm.parse_scm_fallback"]
".git_archival.txt" = "setuptools_scm.git:parse_archival"
".hg_archival.txt" = "setuptools_scm.hg:parse_archival"
-PKG-INFO = "setuptools_scm.hacks:parse_pkginfo"
-pip-egg-info = "setuptools_scm.hacks:parse_pip_egg_info"
-"pyproject.toml" = "setuptools_scm.hacks:fallback_version"
-"setup.py" = "setuptools_scm.hacks:fallback_version"
+PKG-INFO = "setuptools_scm.fallbacks:parse_pkginfo"
+"pyproject.toml" = "setuptools_scm.fallbacks:fallback_version"
+"setup.py" = "setuptools_scm.fallbacks:fallback_version"
[project.entry-points."setuptools_scm.version_scheme"]
"calver-by-date" = "setuptools_scm.version:calver_by_date"
"guess-next-dev" = "setuptools_scm.version:guess_next_dev_version"
diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py
index eccc68f..b29273c 100644
--- a/src/setuptools_scm/__init__.py
+++ b/src/setuptools_scm/__init__.py
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
from typing import NoReturn
from . import _types as _t
+
TEMPLATES = {
".py": """\
# file generated by setuptools_scm
@@ -49,9 +50,9 @@ def dump_version(
target = os.path.normpath(os.path.join(root, write_to))
ext = os.path.splitext(target)[1]
template = template or TEMPLATES.get(ext)
- from ._trace import trace
+ from ._log import log
- trace("dump", write_to, version)
+ log.debug("dump %s into %s", version, write_to)
if template is None:
raise ValueError(
"bad file format: '{}' (of {}) \nonly *.txt and *.py are supported".format(
diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py
index 34745ae..108c86e 100644
--- a/src/setuptools_scm/_config.py
+++ b/src/setuptools_scm/_config.py
@@ -9,17 +9,19 @@ from typing import Any
from typing import Callable
from typing import Pattern
+from . import _log
from . import _types as _t
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 ._overrides import read_toml_overrides
-from ._trace import trace
from ._version_cls import _validate_version_cls
from ._version_cls import _VersionT
from ._version_cls import Version as _Version
+log = _log.log.getChild("config")
+
DEFAULT_TAG_REGEX = re.compile(
r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
)
@@ -44,7 +46,7 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
- trace("abs root", repr(locals()))
+ log.debug("check absolute root=%s relative_to=%s", root, relative_to)
if relative_to:
if (
os.path.isabs(root)
@@ -61,10 +63,10 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
" its the directory %r\n"
"assuming the parent directory was passed" % (relative_to,)
)
- trace("dir", relative_to)
+ log.debug("dir %s", relative_to)
root = os.path.join(relative_to, root)
else:
- trace("file", relative_to)
+ log.debug("file %s", relative_to)
root = os.path.join(os.path.dirname(relative_to), root)
return os.path.abspath(root)
diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py
index 9b5b093..01b48b1 100644
--- a/src/setuptools_scm/_entrypoints.py
+++ b/src/setuptools_scm/_entrypoints.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import warnings
from typing import Any
from typing import Callable
from typing import cast
@@ -8,17 +7,23 @@ from typing import Iterator
from typing import overload
from typing import TYPE_CHECKING
+from typing_extensions import Protocol
+
+from . import _log
from . import version
-from ._trace import trace
if TYPE_CHECKING:
from ._config import Configuration
- from typing_extensions import Protocol
from . import _types as _t
-else:
- Configuration = Any
- class Protocol:
+
+log = _log.log.getChild("entrypoints")
+
+
+class EntrypointProtocol(Protocol):
+ name: str
+
+ def load(self) -> Any:
pass
@@ -34,64 +39,52 @@ def _version_from_entrypoints(
from .discover import iter_matching_entrypoints
- trace("version_from_ep", entrypoint, root)
+ log.debug("version_from_ep %s in %s", entrypoint, root)
for ep in iter_matching_entrypoints(root, entrypoint, config):
fn = ep.load()
maybe_version: version.ScmVersion | None = fn(root, config=config)
- trace(ep, version)
+ log.debug("%s found %r", ep, maybe_version)
if maybe_version is not None:
return maybe_version
return None
try:
- from importlib.metadata import entry_points # type: ignore
- from importlib.metadata import EntryPoint
+ from importlib_metadata import entry_points
+ from importlib_metadata import EntryPoint
except ImportError:
- try:
- from importlib_metadata import entry_points
- from importlib_metadata import EntryPoint
- except ImportError:
- from collections import defaultdict
-
- def entry_points() -> dict[str, list[_t.EntrypointProtocol]]:
- warnings.warn(
- "importlib metadata missing, "
- "this may happen at build time for python3.7"
- )
- return defaultdict(list)
-
- class EntryPoint: # type: ignore
- def __init__(self, *args: Any, **kwargs: Any):
- pass # entry_points() already provides the warning
+ from importlib.metadata import entry_points # type: ignore [no-redef, import]
+ from importlib.metadata import EntryPoint # type: ignore [no-redef]
def iter_entry_points(
group: str, name: str | None = None
-) -> Iterator[_t.EntrypointProtocol]:
- all_eps = entry_points()
- if hasattr(all_eps, "select"):
- eps = all_eps.select(group=group)
- else:
- eps = all_eps[group]
- if name is None:
- return iter(eps)
- return (ep for ep in eps if ep.name == name)
+) -> Iterator[EntrypointProtocol]:
+ eps = entry_points(group=group)
+ res = (
+ eps
+ if name is None
+ else eps.select( # type: ignore [no-untyped-call]
+ name=name,
+ )
+ )
+ return cast(Iterator[EntrypointProtocol], iter(res))
def _get_ep(group: str, name: str) -> Any | None:
- from ._entrypoints import iter_entry_points
-
for ep in iter_entry_points(group, name):
- trace("ep found:", ep.name)
+ log.debug("ep found: %s", ep.name)
return ep.load()
else:
return None
def _get_from_object_reference_str(path: str) -> Any | None:
+ ep: EntrypointProtocol = EntryPoint(
+ path, path, None
+ ) # type: ignore [no-untyped-call]
try:
- return EntryPoint(path, path, None).load()
+ return ep.load()
except (AttributeError, ModuleNotFoundError):
return None
@@ -121,22 +114,22 @@ def _iter_version_schemes(
@overload
def _call_version_scheme(
- version: version.ScmVersion, entypoint: str, given_value: str, default: str
+ version: version.ScmVersion, entrypoint: str, given_value: str, default: str
) -> str:
...
@overload
def _call_version_scheme(
- version: version.ScmVersion, entypoint: str, given_value: str, default: None
+ version: version.ScmVersion, entrypoint: str, given_value: str, default: None
) -> str | None:
...
def _call_version_scheme(
- version: version.ScmVersion, entypoint: str, given_value: str, default: str | None
+ version: version.ScmVersion, entrypoint: str, given_value: str, default: str | None
) -> str | None:
- for scheme in _iter_version_schemes(entypoint, given_value):
+ for scheme in _iter_version_schemes(entrypoint, given_value):
result = scheme(version)
if result is not None:
return result
diff --git a/src/setuptools_scm/_file_finders/__init__.py b/src/setuptools_scm/_file_finders/__init__.py
index 5a7a1c0..5e3304a 100644
--- a/src/setuptools_scm/_file_finders/__init__.py
+++ b/src/setuptools_scm/_file_finders/__init__.py
@@ -6,9 +6,11 @@ from typing import Callable
from typing_extensions import TypeGuard
-from setuptools_scm import _types as _t
-from setuptools_scm._entrypoints import iter_entry_points
-from setuptools_scm._trace import trace
+from .. import _log
+from .. import _types as _t
+from .._entrypoints import iter_entry_points
+
+log = _log.log.getChild("file_finder")
def scm_find_files(
@@ -84,7 +86,7 @@ def is_toplevel_acceptable(toplevel: str | None) -> TypeGuard[str]:
)
ignored = [os.path.normcase(p) for p in ignored]
- trace(toplevel, ignored)
+ log.debug("toplevel: %r\n ignored %s", toplevel, ignored)
return toplevel not in ignored
diff --git a/src/setuptools_scm/_file_finders/git.py b/src/setuptools_scm/_file_finders/git.py
index f007d5b..873b4ba 100644
--- a/src/setuptools_scm/_file_finders/git.py
+++ b/src/setuptools_scm/_file_finders/git.py
@@ -10,8 +10,7 @@ from . import is_toplevel_acceptable
from . import scm_find_files
from .. import _types as _t
from .._run_cmd import run as _run
-from .._trace import trace
-from ..utils import data_from_mime
+from ..integration import data_from_mime
log = logging.getLogger(__name__)
@@ -43,7 +42,7 @@ def _git_toplevel(path: str) -> str | None:
# for this assertion to work. Length of string isn't changed by replace
# ``\\`` is just and escape for `\`
out = cwd[: -len(out)]
- trace("find files toplevel", out)
+ log.debug("find files toplevel %s", out)
return os.path.normcase(os.path.realpath(out.strip()))
except subprocess.CalledProcessError:
# git returned error, we are not in a git repo
@@ -94,7 +93,7 @@ def git_find_files(path: _t.PathT = "") -> list[str]:
return []
fullpath = os.path.abspath(os.path.normpath(path))
if not fullpath.startswith(toplevel):
- trace("toplevel mismatch", toplevel, fullpath)
+ log.warning("toplevel mismatch computed %s vs resolved %s ", toplevel, fullpath)
git_files, git_dirs = _git_ls_files_and_dirs(toplevel)
return scm_find_files(path, git_files, git_dirs)
@@ -112,5 +111,5 @@ def git_archive_find_files(path: _t.PathT = "") -> list[str]:
# Substitutions have not been performed, so not a reliable archive
return []
- trace("git archive detected - fallback to listing all files")
+ log.warning("git archive detected - fallback to listing all files")
return scm_find_files(path, set(), set(), force_all_files=True)
diff --git a/src/setuptools_scm/_file_finders/hg.py b/src/setuptools_scm/_file_finders/hg.py
index 8f777ea..ec8604a 100644
--- a/src/setuptools_scm/_file_finders/hg.py
+++ b/src/setuptools_scm/_file_finders/hg.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import logging
import os
import subprocess
@@ -7,8 +8,9 @@ from .. import _types as _t
from .._file_finders import is_toplevel_acceptable
from .._file_finders import scm_find_files
from .._run_cmd import run as _run
-from .._trace import trace
-from ..utils import data_from_mime
+from ..integration import data_from_mime
+
+log = logging.getLogger(__name__)
def _hg_toplevel(path: str) -> str | None:
@@ -66,5 +68,5 @@ def hg_archive_find_files(path: _t.PathT = "") -> list[str]:
# Ensure file is valid
return []
- trace("hg archive detected - fallback to listing all files")
+ log.warning("hg archive detected - fallback to listing all files")
return scm_find_files(path, set(), set(), force_all_files=True)
diff --git a/src/setuptools_scm/_log.py b/src/setuptools_scm/_log.py
new file mode 100644
index 0000000..35c050d
--- /dev/null
+++ b/src/setuptools_scm/_log.py
@@ -0,0 +1,94 @@
+"""
+logging helpers, supports vendoring
+"""
+from __future__ import annotations
+
+import contextlib
+import logging
+import os
+import sys
+from typing import IO
+from typing import Iterator
+
+log = logging.getLogger(__name__.rsplit(".", 1)[0])
+log.propagate = False
+
+
+class AlwaysStdErrHandler(logging.StreamHandler): # type: ignore[type-arg]
+ def __init___(self) -> None:
+ super().__init__(sys.stderr)
+
+ @property # type: ignore [override]
+ def stream(self) -> IO[str]:
+ return sys.stderr
+
+ @stream.setter
+ def stream(self, value: IO[str]) -> None:
+ assert value is sys.stderr
+
+
+def make_default_handler() -> logging.Handler:
+ try:
+ from rich.console import Console
+
+ console = Console(stderr=True)
+ from rich.logging import RichHandler
+
+ return RichHandler(console=console)
+ except ImportError:
+ handler = AlwaysStdErrHandler()
+ handler.setFormatter(logging.Formatter("%(levelname)s %(name)s %(message)s"))
+ return handler
+
+
+_default_handler = make_default_handler()
+
+log.addHandler(_default_handler)
+
+
+def _default_log_level() -> str | int:
+ val: str = os.environ.get("SETUPTOOLS_SCM_DEBUG", "")
+ level: str | int
+ if val:
+ level = logging.DEBUG
+ levelname: str | int = logging.getLevelName(val)
+ if isinstance(levelname, int):
+ level = val
+ else:
+ level = logging.WARNING
+ else:
+ level = logging.WARNING
+ return level
+
+
+log.setLevel(_default_log_level())
+
+
+@contextlib.contextmanager
+def defer_to_pytest() -> Iterator[None]:
+ log.propagate = True
+ old_level = log.level
+ log.setLevel(logging.NOTSET)
+ log.removeHandler(_default_handler)
+ try:
+ yield
+ finally:
+ log.addHandler(_default_handler)
+ log.propagate = False
+ log.setLevel(old_level)
+
+
+@contextlib.contextmanager
+def enable_debug(handler: logging.Handler = _default_handler) -> Iterator[None]:
+ log.addHandler(handler)
+ old_level = log.level
+ log.setLevel(logging.DEBUG)
+ old_handler_level = handler.level
+ handler.setLevel(logging.DEBUG)
+ try:
+ yield
+ finally:
+ log.setLevel(old_level)
+ handler.setLevel(old_handler_level)
+ if handler is not _default_handler:
+ log.removeHandler(handler)
diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py
index f08f170..6279377 100644
--- a/src/setuptools_scm/_overrides.py
+++ b/src/setuptools_scm/_overrides.py
@@ -4,9 +4,11 @@ import os
from typing import Any
from . import _config
+from . import _log
from . import version
from ._integration.pyproject_reading import lazy_toml_load
-from ._trace import trace
+
+log = _log.log.getChild("overrides")
PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}"
@@ -30,7 +32,7 @@ def _read_pretended_version_for(
tries ``SETUPTOOLS_SCM_PRETEND_VERSION``
and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME``
"""
- trace("dist name:", config.dist_name)
+ log.debug("dist name: %s", config.dist_name)
pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name)
diff --git a/src/setuptools_scm/_run_cmd.py b/src/setuptools_scm/_run_cmd.py
index b528504..b1e5f1a 100644
--- a/src/setuptools_scm/_run_cmd.py
+++ b/src/setuptools_scm/_run_cmd.py
@@ -3,11 +3,73 @@ from __future__ import annotations
import os
import shlex
import subprocess
+import textwrap
+import warnings
+from typing import Callable
from typing import Mapping
+from typing import overload
+from typing import Sequence
+from typing import TYPE_CHECKING
+from typing import TypeVar
-from . import _trace
+from . import _log
from . import _types as _t
+if TYPE_CHECKING:
+ BaseCompletedProcess = subprocess.CompletedProcess[str]
+else:
+ BaseCompletedProcess = subprocess.CompletedProcess
+
+
+log = _log.log.getChild("run_cmd")
+
+PARSE_RESULT = TypeVar("PARSE_RESULT")
+T = TypeVar("T")
+
+
+class CompletedProcess(BaseCompletedProcess):
+ @classmethod
+ def from_raw(
+ cls, input: BaseCompletedProcess, strip: bool = True
+ ) -> CompletedProcess:
+ return cls(
+ args=input.args,
+ returncode=input.returncode,
+ stdout=input.stdout.strip() if strip and input.stdout else input.stdout,
+ stderr=input.stderr.strip() if strip and input.stderr else input.stderr,
+ )
+
+ @overload
+ def parse_success(
+ self,
+ parse: Callable[[str], PARSE_RESULT],
+ default: None = None,
+ error_msg: str | None = None,
+ ) -> PARSE_RESULT | None:
+ ...
+
+ @overload
+ def parse_success(
+ self,
+ parse: Callable[[str], PARSE_RESULT],
+ default: T,
+ error_msg: str | None = None,
+ ) -> PARSE_RESULT | T:
+ ...
+
+ def parse_success(
+ self,
+ parse: Callable[[str], PARSE_RESULT],
+ default: T | None = None,
+ error_msg: str | None = None,
+ ) -> PARSE_RESULT | T | None:
+ if self.returncode:
+ if error_msg:
+ log.warning("%s %s", error_msg, self)
+ return default
+ else:
+ return parse(self.stdout)
+
def no_git_env(env: Mapping[str, str]) -> dict[str, str]:
# adapted from pre-commit
@@ -21,7 +83,7 @@ def no_git_env(env: Mapping[str, str]) -> dict[str, str]:
# GIT_INDEX_FILE: Causes 'error invalid object ...' during commit
for k, v in env.items():
if k.startswith("GIT_"):
- _trace.trace(k, v)
+ log.debug("%s: %s", k, v)
return {
k: v
for k, v in env.items()
@@ -66,17 +128,17 @@ def run(
trace: bool = True,
timeout: int = 20,
check: bool = False,
-) -> subprocess.CompletedProcess[str]:
+) -> CompletedProcess:
if isinstance(cmd, str):
cmd = shlex.split(cmd)
else:
cmd = [os.fspath(x) for x in cmd]
- if trace:
- _trace.trace_command(cmd, cwd)
+ cmd_4_trace = " ".join(map(_unsafe_quote_for_display, cmd))
+ log.debug("at %s\n $ %s ", cwd, cmd_4_trace)
res = subprocess.run(
cmd,
capture_output=True,
- cwd=str(cwd),
+ cwd=os.fspath(cwd),
env=dict(
avoid_pip_isolation(no_git_env(os.environ)),
# os.environ,
@@ -88,17 +150,45 @@ def run(
text=True,
timeout=timeout,
)
- if strip:
- if res.stdout:
- res.stdout = ensure_stripped_str(res.stdout)
- res.stderr = ensure_stripped_str(res.stderr)
+
+ res = CompletedProcess.from_raw(res, strip=strip)
if trace:
if res.stdout:
- _trace.trace("out:\n", res.stdout, indent=True)
+ log.debug("out:\n%s", textwrap.indent(res.stdout, " "))
if res.stderr:
- _trace.trace("err:\n", res.stderr, indent=True)
+ log.debug("err:\n%s", textwrap.indent(res.stderr, " "))
if res.returncode:
- _trace.trace("ret:", res.returncode)
+ log.debug("ret: %s", res.returncode)
if check:
res.check_returncode()
return res
+
+
+def _unsafe_quote_for_display(item: _t.PathT) -> str:
+ # give better results than shlex.join in our cases
+ text = os.fspath(item)
+ return text if all(c not in text for c in " {[:") else f'"{text}"'
+
+
+def has_command(
+ name: str, args: Sequence[str] = ["version"], warn: bool = True
+) -> bool:
+ try:
+ p = run([name, *args], cwd=".", timeout=5)
+ except OSError as e:
+ log.warning("command %s missing: %s", name, e)
+ res = False
+ except subprocess.TimeoutExpired as e:
+ log.warning("command %s timed out %s", name, e)
+ res = False
+
+ else:
+ res = not p.returncode
+ if not res and warn:
+ warnings.warn("%r was not found" % name, category=RuntimeWarning)
+ return res
+
+
+def require_command(name: str) -> None:
+ if not has_command(name, warn=False):
+ raise OSError(f"{name!r} was not found")
diff --git a/src/setuptools_scm/_trace.py b/src/setuptools_scm/_trace.py
deleted file mode 100644
index cf5fcfa..0000000
--- a/src/setuptools_scm/_trace.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from __future__ import annotations
-
-import os
-import sys
-import textwrap
-from typing import Sequence
-
-from . import _types as _t
-
-DEBUG: bool = bool(os.environ.get("SETUPTOOLS_SCM_DEBUG"))
-
-
-def trace(*k: object, indent: bool = False) -> None:
- if not DEBUG:
- if indent and len(k) > 1:
- k = (k[0],) + tuple(textwrap.indent(str(s), " ") for s in k[1:])
- print(*k, file=sys.stderr, flush=True)
-
-
-def _unsafe_quote_for_display(item: _t.PathT) -> str:
- # give better results than shlex.join in our cases
- text = os.fspath(item)
- return text if all(c not in text for c in " {[:") else f'"{text}"'
-
-
-def trace_command(cmd: Sequence[_t.PathT], cwd: _t.PathT) -> None:
- if not DEBUG:
- return
- cmd_4_trace = " ".join(map(_unsafe_quote_for_display, cmd))
- trace(f"---\n > {cwd}\\$ ", cmd_4_trace, indent=True)
diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py
index 7cd57a1..fdcd2dd 100644
--- a/src/setuptools_scm/_types.py
+++ b/src/setuptools_scm/_types.py
@@ -1,19 +1,17 @@
from __future__ import annotations
import os
-from typing import Any
from typing import Callable
from typing import List
from typing import Sequence
from typing import Tuple
-from typing import TypeVar
+from typing import TYPE_CHECKING
from typing import Union
-from typing_extensions import ParamSpec
-from typing_extensions import Protocol
from typing_extensions import TypeAlias
-from . import version
+if TYPE_CHECKING:
+ from . import version
PathT: TypeAlias = Union["os.PathLike[str]", str]
@@ -22,24 +20,3 @@ CMD_TYPE: TypeAlias = Union[Sequence[PathT], str]
VERSION_SCHEME: TypeAlias = Union[str, Callable[["version.ScmVersion"], str]]
VERSION_SCHEMES: TypeAlias = Union[List[str], Tuple[str, ...], VERSION_SCHEME]
SCMVERSION: TypeAlias = "version.ScmVersion"
-
-
-class EntrypointProtocol(Protocol):
- name: str
-
- def load(self) -> Any:
- pass
-
-
-T = TypeVar("T")
-T2 = TypeVar("T2")
-P = ParamSpec("P")
-
-
-def transfer_input_args(
- template: Callable[P, T],
-) -> Callable[[Callable[..., T]], Callable[P, T]]:
- def decorate(func: Callable[..., T2]) -> Callable[P, T2]:
- return func
-
- return decorate
diff --git a/src/setuptools_scm/_version_cls.py b/src/setuptools_scm/_version_cls.py
index f6e87a8..e62c9fa 100644
--- a/src/setuptools_scm/_version_cls.py
+++ b/src/setuptools_scm/_version_cls.py
@@ -38,8 +38,9 @@ def _version_as_tuple(version_str: str) -> tuple[int | str, ...]:
try:
parsed_version = Version(version_str)
except InvalidVersion:
- log = getLogger("setuptools_scm")
- log.exception("failed to parse version %s", version_str)
+ log = getLogger(__name__).parent
+ assert log is not None
+ log.error("failed to parse version %s", version_str)
return (version_str,)
else:
version_fields: tuple[int | str, ...] = parsed_version.release
diff --git a/src/setuptools_scm/discover.py b/src/setuptools_scm/discover.py
index 2bd01e9..85dd31f 100644
--- a/src/setuptools_scm/discover.py
+++ b/src/setuptools_scm/discover.py
@@ -1,32 +1,28 @@
from __future__ import annotations
import os
+from pathlib import Path
from typing import Iterable
from typing import Iterator
+from . import _entrypoints
+from . import _log
from . import _types as _t
from ._config import Configuration
-from ._trace import trace
+log = _log.log.getChild("discover")
-def walk_potential_roots(
- root: _t.PathT, search_parents: bool = True
-) -> Iterator[_t.PathT]:
+
+def walk_potential_roots(root: _t.PathT, search_parents: bool = True) -> Iterator[Path]:
"""
Iterate though a path and each of its parents.
:param root: File path.
:param search_parents: If ``False`` the parents are not considered.
"""
-
- if not search_parents:
- yield root
- return
-
- tail = root
-
- while tail:
- yield root
- root, tail = os.path.split(root)
+ root = Path(root)
+ yield root
+ if search_parents:
+ yield from root.parents
def match_entrypoint(root: _t.PathT, name: str) -> bool:
@@ -40,14 +36,14 @@ def match_entrypoint(root: _t.PathT, name: str) -> bool:
if os.path.exists(os.path.join(root, name)):
if not os.path.isabs(name):
return True
- trace("ignoring bad ep", name)
+ log.debug("ignoring bad ep %s", name)
return False
def iter_matching_entrypoints(
root: _t.PathT, entrypoint: str, config: Configuration
-) -> Iterable[_t.EntrypointProtocol]:
+) -> Iterable[_entrypoints.EntrypointProtocol]:
"""
Consider different entry-points in ``root`` and optionally its parents.
:param root: File path.
@@ -56,12 +52,12 @@ def iter_matching_entrypoints(
read ``search_parent_directories``, write found parent to ``parent``.
"""
- trace("looking for ep", entrypoint, root)
+ log.debug("looking for ep %s in %s", entrypoint, root)
from ._entrypoints import iter_entry_points
for wd in walk_potential_roots(root, config.search_parent_directories):
for ep in iter_entry_points(entrypoint):
if match_entrypoint(wd, ep.name):
- trace("found ep", ep, "in", wd)
+ log.debug("found ep %s in %s", ep, wd)
config.parent = wd
yield ep
diff --git a/src/setuptools_scm/hacks.py b/src/setuptools_scm/fallbacks.py
index 5aaa0db..e1ea60c 100644
--- a/src/setuptools_scm/hacks.py
+++ b/src/setuptools_scm/fallbacks.py
@@ -1,23 +1,26 @@
from __future__ import annotations
+import logging
import os
+from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import _types as _t
from . import Configuration
-from .utils import data_from_mime
-from ._trace import trace
+from .integration import data_from_mime
from .version import meta
from .version import ScmVersion
from .version import tag_to_version
+log = logging.getLogger(__name__)
+
_UNKNOWN = "UNKNOWN"
def parse_pkginfo(root: _t.PathT, config: Configuration) -> ScmVersion | None:
- pkginfo = os.path.join(root, "PKG-INFO")
- trace("pkginfo", pkginfo)
+ pkginfo = Path(root) / "PKG-INFO"
+ log.debug("pkginfo %s", pkginfo)
data = data_from_mime(pkginfo)
version = data.get("Version", _UNKNOWN)
if version != _UNKNOWN:
@@ -26,17 +29,6 @@ def parse_pkginfo(root: _t.PathT, config: Configuration) -> ScmVersion | None:
return None
-def parse_pip_egg_info(root: _t.PathT, config: Configuration) -> ScmVersion | None:
- pipdir = os.path.join(root, "pip-egg-info")
- if not os.path.isdir(pipdir):
- return None
- items = os.listdir(pipdir)
- trace("pip-egg-info", pipdir, items)
- if not items:
- return None
- return parse_pkginfo(os.path.join(pipdir, items[0]), config=config)
-
-
def fallback_version(root: _t.PathT, config: Configuration) -> ScmVersion | None:
if config.parentdir_prefix_version is not None:
_, parent_name = os.path.split(os.path.abspath(root))
@@ -47,6 +39,6 @@ def fallback_version(root: _t.PathT, config: Configuration) -> ScmVersion | None
if version is not None:
return meta(str(version), preformatted=True, config=config)
if config.fallback_version is not None:
- trace("FALLBACK")
+ log.debug("FALLBACK %s", config.fallback_version)
return meta(config.fallback_version, preformatted=True, config=config)
return None
diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py
index 484bd4d..d1da7f8 100644
--- a/src/setuptools_scm/git.py
+++ b/src/setuptools_scm/git.py
@@ -1,31 +1,34 @@
from __future__ import annotations
+import dataclasses
+import logging
import os
import re
+import shlex
import warnings
from datetime import date
from datetime import datetime
-from os.path import isfile
-from os.path import join
from os.path import samefile
+from pathlib import Path
from typing import Callable
+from typing import Sequence
from typing import TYPE_CHECKING
from . import _types as _t
from . import Configuration
-from ._trace import trace
+from . import discover
+from ._run_cmd import CompletedProcess as _CompletedProcess
+from ._run_cmd import require_command as _require_command
+from ._run_cmd import run as _run
+from .integration import data_from_mime
from .scm_workdir import Workdir
-from .utils import _CmdResult
-from .utils import data_from_mime
-from .utils import do_ex
-from .utils import require_command
from .version import meta
from .version import ScmVersion
from .version import tag_to_version
if TYPE_CHECKING:
from . import hg_git
-
+log = logging.getLogger(__name__)
REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b")
DESCRIBE_UNSUPPORTED = "%(describe"
@@ -44,89 +47,105 @@ DEFAULT_DESCRIBE = [
]
+def run_git(
+ args: Sequence[str | os.PathLike[str]], repo: Path, *, check: bool = False
+) -> _CompletedProcess:
+ return _run(["git", "--git-dir", repo / ".git", *args], cwd=repo, check=check)
+
+
class GitWorkdir(Workdir):
"""experimental, may change at any time"""
- COMMAND = "git"
-
@classmethod
def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None:
- require_command(cls.COMMAND)
- wd = os.path.abspath(wd)
- git_dir = join(wd, ".git")
- real_wd, _, ret = do_ex(
- ["git", "--git-dir", git_dir, "rev-parse", "--show-prefix"], wd
- )
- real_wd = real_wd[:-1] # remove the trailing pathsep
- if ret:
+ wd = Path(wd).resolve()
+ real_wd = run_git(["rev-parse", "--show-prefix"], wd).parse_success(parse=str)
+ if real_wd is None:
return None
+ else:
+ real_wd = real_wd[:-1] # remove the trailing pathsep
+
if not real_wd:
- real_wd = wd
+ real_wd = os.fspath(wd)
else:
- assert wd.replace("\\", "/").endswith(real_wd)
+ str_wd = os.fspath(wd)
+ assert str_wd.replace("\\", "/").endswith(real_wd)
# In windows wd contains ``\`` which should be replaced by ``/``
# for this assertion to work. Length of string isn't changed by replace
# ``\\`` is just and escape for `\`
- real_wd = wd[: -len(real_wd)]
- trace("real root", real_wd)
+ real_wd = str_wd[: -len(real_wd)]
+ log.debug("real root %s", real_wd)
if not samefile(real_wd, wd):
return None
- return cls(real_wd)
-
- def do_ex_git(self, cmd: list[str]) -> _CmdResult:
- return self.do_ex(["git", "--git-dir", join(self.path, ".git")] + cmd)
+ return cls(Path(real_wd))
def is_dirty(self) -> bool:
- out, _, _ = self.do_ex_git(["status", "--porcelain", "--untracked-files=no"])
- return bool(out)
+ return run_git(
+ ["status", "--porcelain", "--untracked-files=no"], self.path
+ ).parse_success(
+ parse=bool,
+ default=False,
+ )
def get_branch(self) -> str | None:
- branch, err, ret = self.do_ex_git(["rev-parse", "--abbrev-ref", "HEAD"])
- if ret:
- trace("branch err", branch, err, ret)
- branch, err, ret = self.do_ex_git(["symbolic-ref", "--short", "HEAD"])
- if ret:
- trace("branch err (symbolic-ref)", branch, err, ret)
- return None
- return branch
+ return run_git(
+ ["rev-parse", "--abbrev-ref", "HEAD"],
+ self.path,
+ ).parse_success(
+ parse=str,
+ error_msg="branch err (abbrev-err)",
+ ) or run_git(
+ ["symbolic-ref", "--short", "HEAD"],
+ self.path,
+ ).parse_success(
+ parse=str,
+ error_msg="branch err (symbolic-ref)",
+ )
def get_head_date(self) -> date | None:
- timestamp, err, ret = self.do_ex_git(
- ["-c", "log.showSignature=false", "log", "-n", "1", "HEAD", "--format=%cI"]
+ def parse_timestamp(timestamp_text: str) -> date | None:
+ if "%c" in timestamp_text:
+ log.warning("git too old -> timestamp is %r", timestamp_text)
+ return None
+ return datetime.fromisoformat(timestamp_text).date()
+
+ res = run_git(
+ [
+ *("-c", "log.showSignature=false"),
+ *("log", "-n", "1", "HEAD"),
+ "--format=%cI",
+ ],
+ self.path,
+ )
+ return res.parse_success(
+ parse=parse_timestamp,
+ error_msg="logging the iso date for head failed",
+ default=None,
)
- if ret:
- trace("timestamp err", timestamp, err, ret)
- return None
- # TODO, when dropping python3.6 use fromiso
- date_part = timestamp.split("T")[0]
- if "%c" in date_part:
- trace("git too old -> timestamp is ", timestamp)
- return None
- return datetime.strptime(date_part, r"%Y-%m-%d").date()
def is_shallow(self) -> bool:
- return isfile(join(self.path, ".git/shallow"))
+ return self.path.joinpath(".git/shallow").is_file()
def fetch_shallow(self) -> None:
- self.do_ex_git(["fetch", "--unshallow"])
+ run_git(["fetch", "--unshallow"], self.path, check=True)
def node(self) -> str | None:
- node, _, ret = self.do_ex_git(["rev-parse", "--verify", "--quiet", "HEAD"])
- if not ret:
+ def _unsafe_short_node(node: str) -> str:
return node[:7]
- else:
- return None
+
+ return run_git(
+ ["rev-parse", "--verify", "--quiet", "HEAD"], self.path
+ ).parse_success(
+ parse=_unsafe_short_node,
+ )
def count_all_nodes(self) -> int:
- revs, _, _ = self.do_ex_git(["rev-list", "HEAD"])
- return revs.count("\n") + 1
+ res = run_git(["rev-list", "HEAD"], self.path)
+ return res.stdout.count("\n") + 1
- def default_describe(self) -> _CmdResult:
- git_dir = join(self.path, ".git")
- return self.do_ex(
- DEFAULT_DESCRIBE[:1] + ["--git-dir", git_dir] + DEFAULT_DESCRIBE[1:]
- )
+ def default_describe(self) -> _CompletedProcess:
+ return run_git(DEFAULT_DESCRIBE[1:], self.path)
def warn_on_shallow(wd: GitWorkdir) -> None:
@@ -150,7 +169,7 @@ def fail_on_shallow(wd: GitWorkdir) -> None:
)
-def get_working_directory(config: Configuration, root: str) -> GitWorkdir | None:
+def get_working_directory(config: Configuration, root: _t.PathT) -> GitWorkdir | None:
"""
Return the working directory (``GitWorkdir``).
"""
@@ -158,14 +177,18 @@ def get_working_directory(config: Configuration, root: str) -> GitWorkdir | None
if config.parent: # todo broken
return GitWorkdir.from_potential_worktree(config.parent)
- if config.search_parent_directories:
- return search_parent(root)
+ for potential_root in discover.walk_potential_roots(
+ root, search_parents=config.search_parent_directories
+ ):
+ potential_wd = GitWorkdir.from_potential_worktree(potential_root)
+ if potential_wd is not None:
+ return potential_wd
return GitWorkdir.from_potential_worktree(root)
def parse(
- root: str,
+ root: _t.PathT,
config: Configuration,
describe_command: str | list[str] | None = None,
pre_parse: Callable[[GitWorkdir], None] = warn_on_shallow,
@@ -173,6 +196,7 @@ def parse(
"""
:param pre_parse: experimental pre_parse action, may change at any time
"""
+ _require_command("git")
wd = get_working_directory(config, root)
if wd:
return _git_parse_inner(
@@ -182,6 +206,34 @@ def parse(
return None
+def version_from_describe(
+ wd: GitWorkdir | hg_git.GitWorkdirHgClient,
+ config: Configuration,
+ describe_command: _t.CMD_TYPE | None,
+) -> ScmVersion | None:
+ pass
+
+ if config.git_describe_command is not None:
+ describe_command = config.git_describe_command
+
+ if describe_command is not None:
+ if isinstance(describe_command, str):
+ describe_command = shlex.split(describe_command)
+ # todo: figure how ot ensure git with gitdir gets correctly invoked
+ if describe_command[0] == "git":
+ describe_res = run_git(describe_command[1:], wd.path)
+ else:
+ describe_res = _run(describe_command, wd.path)
+ else:
+ describe_res = wd.default_describe()
+
+ def parse_describe(output: str) -> ScmVersion:
+ tag, distance, node, dirty = _git_parse_describe(output)
+ return meta(tag=tag, distance=distance, dirty=dirty, node=node, config=config)
+
+ return describe_res.parse_success(parse=parse_describe)
+
+
def _git_parse_inner(
config: Configuration,
wd: GitWorkdir | hg_git.GitWorkdirHgClient,
@@ -191,47 +243,30 @@ def _git_parse_inner(
if pre_parse:
pre_parse(wd)
- if config.git_describe_command is not None:
- describe_command = config.git_describe_command
+ version = version_from_describe(wd, config, describe_command)
- if describe_command is not None:
- out, _, ret = wd.do_ex(describe_command)
- else:
- out, _, ret = wd.default_describe()
- distance: int | None
- node: str | None
- if ret == 0:
- tag, distance, node, dirty = _git_parse_describe(out)
- if distance == 0 and not dirty:
- distance = None
- else:
+ if version is None:
# If 'git git_describe_command' failed, try to get the information otherwise.
- tag = "0.0"
+ tag = config.version_cls("0.0")
node = wd.node()
if node is None:
distance = 0
+ dirty = True
else:
distance = wd.count_all_nodes()
node = "g" + node
- dirty = wd.is_dirty()
-
+ dirty = wd.is_dirty()
+ version = meta(
+ tag=tag, distance=distance, dirty=dirty, node=node, config=config
+ )
branch = wd.get_branch()
node_date = wd.get_head_date() or date.today()
-
- return meta(
- tag,
- branch=branch,
- node=node,
- node_date=node_date,
- distance=distance,
- dirty=dirty,
- config=config,
- )
+ return dataclasses.replace(version, branch=branch, node_date=node_date)
def _git_parse_describe(
describe_output: str,
-) -> tuple[str, int | None, str | None, bool]:
+) -> tuple[str, int, str | None, bool]:
# 'describe_output' looks e.g. like 'v1.5.0-0-g4060507' or
# 'v1.15.1rc1-37-g9bd1298-dirty'.
# It may also just be a bare tag name if this is a tagged commit and we are
@@ -246,7 +281,7 @@ def _git_parse_describe(
split = describe_output.rsplit("-", 2)
if len(split) < 3: # probably a tagged commit
tag = describe_output
- number = None
+ number = 0
node = None
else:
tag, number_, node = split
@@ -254,38 +289,11 @@ def _git_parse_describe(
return tag, number, node, dirty
-def search_parent(dirname: _t.PathT) -> GitWorkdir | None:
- """
- Walk up the path to find the `.git` directory.
- :param dirname: Directory from which to start searching.
- """
-
- # Code based on:
- # https://github.com/gitpython-developers/GitPython/blob/main/git/repo/base.py
-
- curpath = os.path.abspath(dirname)
-
- while curpath:
- try:
- wd = GitWorkdir.from_potential_worktree(curpath)
- except Exception:
- wd = None
-
- if wd is not None:
- return wd
-
- curpath, tail = os.path.split(curpath)
-
- if not tail:
- return None
- return None
-
-
def archival_to_version(
data: dict[str, str], config: Configuration
) -> ScmVersion | None:
node: str | None
- trace("data", data)
+ log.debug("data %s", data)
archival_describe = data.get("describe-name", DESCRIBE_UNSUPPORTED)
if DESCRIBE_UNSUPPORTED in archival_describe:
warnings.warn("git archive did not support describe output")
@@ -294,7 +302,7 @@ def archival_to_version(
return meta(
tag,
config=config,
- distance=None if number == 0 else number,
+ distance=number,
node=node,
)
@@ -307,7 +315,7 @@ def archival_to_version(
if node is None:
return None
elif "$FORMAT" in node.upper():
- warnings.warn("unexported git archival found")
+ warnings.warn("unprocessed git archival found (no export subst applied)")
return None
else:
return meta("0.0", node=node, config=config)
diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py
index e7295bc..522dfb6 100644
--- a/src/setuptools_scm/hg.py
+++ b/src/setuptools_scm/hg.py
@@ -1,16 +1,15 @@
from __future__ import annotations
import datetime
+import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING
from . import Configuration
-from ._trace import trace
from ._version_cls import Version
+from .integration import data_from_mime
from .scm_workdir import Workdir
-from .utils import data_from_mime
-from .utils import require_command
from .version import meta
from .version import ScmVersion
from .version import tag_to_version
@@ -18,19 +17,18 @@ from .version import tag_to_version
if TYPE_CHECKING:
from . import _types as _t
-from ._run_cmd import run as _run
+from ._run_cmd import run as _run, require_command as _require_command
+log = logging.getLogger(__name__)
-class HgWorkdir(Workdir):
- COMMAND = "hg"
+class HgWorkdir(Workdir):
@classmethod
def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None:
- require_command(cls.COMMAND)
- res = _run("hg root", wd)
+ res = _run(["hg", "root"], wd)
if res.returncode:
return None
- return cls(res.stdout)
+ return cls(Path(res.stdout))
def get_meta(self, config: Configuration) -> ScmVersion | None:
node: str
@@ -45,16 +43,22 @@ class HgWorkdir(Workdir):
# mainly used to emulate Git branches, which is already supported with
# the dedicated class GitWorkdirHgClient)
- branch, dirty_str, dirty_date = self.do(
- ["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"]
- ).split("\n")
+ branch, dirty_str, dirty_date = _run(
+ ["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"],
+ cwd=self.path,
+ check=True,
+ ).stdout.split("\n")
dirty = bool(int(dirty_str))
node_date = datetime.date.fromisoformat(dirty_date if dirty else node_date_str)
- if node.count("0") == len(node):
- trace("initial node", self.path)
+ if node == "0" * len(node):
+ log.debug("initial node %s", self.path)
return meta(
- "0.0", config=config, dirty=dirty, branch=branch, node_date=node_date
+ Version("0.0"),
+ config=config,
+ dirty=dirty,
+ branch=branch,
+ node_date=node_date,
)
node = "h" + node[:7]
@@ -97,7 +101,7 @@ class HgWorkdir(Workdir):
return meta(tag, config=config, node_date=node_date)
except ValueError as e:
- trace("error", e)
+ log.exception("error %s", e)
pass # unpacking failed, old hg
return None
@@ -139,6 +143,7 @@ class HgWorkdir(Workdir):
def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None:
+ _require_command("hg")
if os.path.exists(os.path.join(root, ".hg/git")):
res = _run(["hg", "path"], root)
if not res.returncode:
@@ -162,7 +167,7 @@ def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None:
def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersion:
- trace("data", data)
+ log.debug("data %s", data)
node = data.get("node", "")[:12]
if node:
node = "h" + node
@@ -176,7 +181,7 @@ def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersi
config=config,
)
else:
- return meta("0.0", node=node, config=config)
+ return meta(config.version_cls("0.0"), node=node, config=config)
def parse_archival(root: _t.PathT, config: Configuration) -> ScmVersion:
diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py
index f7ea883..b6c3036 100644
--- a/src/setuptools_scm/hg_git.py
+++ b/src/setuptools_scm/hg_git.py
@@ -1,19 +1,25 @@
from __future__ import annotations
+import logging
import os
from contextlib import suppress
from datetime import date
-from datetime import datetime
+from pathlib import Path
from . import _types as _t
-from ._trace import trace
+from ._run_cmd import CompletedProcess as _CompletedProcess
+from ._run_cmd import require_command
+from ._run_cmd import run as _run
from .git import GitWorkdir
from .hg import HgWorkdir
-from .utils import _CmdResult
-from .utils import do_ex
-from .utils import require_command
-_FAKE_GIT_DESCRIBE_ERROR = _CmdResult("<>hg git failed", "", 1)
+log = logging.getLogger(__name__)
+
+_FAKE_GIT_DESCRIBE_ERROR = _CompletedProcess(
+ "fake git describe output for hg",
+ 1,
+ "<>hg git failed to describe",
+)
class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
@@ -21,29 +27,27 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
@classmethod
def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None:
- require_command(cls.COMMAND)
- root, _, ret = do_ex(["hg", "root"], wd)
- if ret:
+ require_command("hg")
+ res = _run(["hg", "root"], cwd=wd).parse_success(parse=Path)
+ if res is None:
return None
- return cls(root)
+ return cls(res)
def is_dirty(self) -> bool:
- out, _, _ = self.do_ex('hg id -T "{dirty}"')
- return bool(out)
+ res = _run(["hg", "id", "-T", "{dirty}"], cwd=self.path, check=True)
+ return bool(res.stdout)
def get_branch(self) -> str | None:
- res = self.do_ex('hg id -T "{bookmarks}"')
+ res = _run(["hg", "id", "-T", "{bookmarks}"], cwd=self.path)
if res.returncode:
- trace("branch err", res)
+ log.info("branch err %s", res)
return None
- return res.out
+ return res.stdout
def get_head_date(self) -> date | None:
- date_part, err, ret = self.do_ex('hg log -r . -T "{shortdate(date)}"')
- if ret:
- trace("head date err", date_part, err, ret)
- return None
- return datetime.strptime(date_part, r"%Y-%m-%d").date()
+ return _run('hg log -r . -T "{shortdate(date)}"', cwd=self.path).parse_success(
+ parse=date.fromisoformat, error_msg="head date err"
+ )
def is_shallow(self) -> bool:
return False
@@ -52,11 +56,11 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
pass
def get_hg_node(self) -> str | None:
- node, _, ret = self.do_ex('hg log -r . -T "{node}"')
- if not ret:
- return node
- else:
+ res = _run('hg log -r . -T "{node}"', cwd=self.path)
+ if res.returncode:
return None
+ else:
+ return res.stdout
def _hg2git(self, hg_node: str) -> str | None:
with suppress(FileNotFoundError):
@@ -76,11 +80,11 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
if git_node is None:
# trying again after hg -> git
- self.do_ex("hg gexport")
+ _run(["hg", "gexport"], cwd=self.path)
git_node = self._hg2git(hg_node)
if git_node is None:
- trace("Cannot get git node so we use hg node", hg_node)
+ log.debug("Cannot get git node so we use hg node %s", hg_node)
if hg_node == "0" * len(hg_node):
# mimic Git behavior
@@ -91,17 +95,17 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
return git_node[:7]
def count_all_nodes(self) -> int:
- revs, _, _ = self.do_ex(["hg", "log", "-r", "ancestors(.)", "-T", "."])
- return len(revs)
+ res = _run(["hg", "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path)
+ return len(res.stdout)
- def default_describe(self) -> _CmdResult:
+ def default_describe(self) -> _CompletedProcess:
"""
Tentative to reproduce the output of
`git describe --dirty --tags --long --match *[0-9]*`
"""
- hg_tags_str, _, ret = self.do_ex(
+ res = _run(
[
"hg",
"log",
@@ -109,16 +113,17 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
"(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))",
"-T",
"{tags}{if(tags, ' ', '')}",
- ]
+ ],
+ cwd=self.path,
)
- if ret:
+ if res.returncode:
return _FAKE_GIT_DESCRIBE_ERROR
- hg_tags: list[str] = hg_tags_str.split()
+ hg_tags: list[str] = res.stdout.split()
if not hg_tags:
return _FAKE_GIT_DESCRIBE_ERROR
- with open(os.path.join(self.path, ".hg/git-tags")) as fp:
+ with self.path.joinpath(".hg/git-tags").open() as fp:
git_tags: dict[str, str] = dict(line.split()[::-1] for line in fp)
tag: str
@@ -127,13 +132,13 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
tag = hg_tag
break
else:
- trace("tag not found", hg_tags, git_tags)
+ logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags)
return _FAKE_GIT_DESCRIBE_ERROR
- out, _, ret = self.do_ex(["hg", "log", "-r", f"'{tag}'::.", "-T", "."])
- if ret:
+ res = _run(["hg", "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path)
+ if res.returncode:
return _FAKE_GIT_DESCRIBE_ERROR
- distance = len(out) - 1
+ distance = len(res.stdout) - 1
node = self.node()
assert node is not None
@@ -141,5 +146,10 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
if self.is_dirty():
desc += "-dirty"
- trace("desc", desc)
- return _CmdResult(desc, "", 0)
+ log.debug("faked describe %r", desc)
+ return _CompletedProcess(
+ ["setuptools-scm", "faked", "describe"],
+ returncode=0,
+ stdout=desc,
+ stderr="",
+ )
diff --git a/src/setuptools_scm/integration.py b/src/setuptools_scm/integration.py
index 1d600e2..9df4f3e 100644
--- a/src/setuptools_scm/integration.py
+++ b/src/setuptools_scm/integration.py
@@ -1,7 +1,10 @@
from __future__ import annotations
+import logging
import os
+import textwrap
import warnings
+from pathlib import Path
from typing import Any
from typing import Callable
from typing import TYPE_CHECKING
@@ -9,14 +12,15 @@ from typing import TYPE_CHECKING
import setuptools
from . import _get_version
+from . import _types as _t
from . import _version_missing
from . import Configuration
from ._integration.setuptools import (
read_dist_name_from_setup_cfg as _read_dist_name_from_setup_cfg,
)
-from ._trace import trace
from ._version_cls import _validate_version_cls
+log = logging.getLogger(__name__)
if TYPE_CHECKING:
pass
@@ -82,11 +86,11 @@ def version_keyword(
if dist.metadata.version is not None:
warnings.warn(f"version of {dist_name} already set")
return
- trace(
- "version keyword",
+ log.debug(
+ "version keyword %r",
vars(dist.metadata),
)
- trace("dist", id(dist), id(dist.metadata))
+ log.debug("dist %s %s", id(dist), id(dist.metadata))
if dist_name is None:
dist_name = _read_dist_name_from_setup_cfg()
@@ -98,11 +102,11 @@ def version_keyword(
def infer_version(dist: setuptools.Distribution) -> None:
- trace(
- "finalize hook",
+ log.debug(
+ "finalize hook %r",
vars(dist.metadata),
)
- trace("dist", id(dist), id(dist.metadata))
+ log.debug("dist %s %s", id(dist), id(dist.metadata))
if dist.metadata.version is not None:
return # metadata already added by hook
dist_name = dist.metadata.name
@@ -115,6 +119,16 @@ def infer_version(dist: setuptools.Distribution) -> None:
try:
config = Configuration.from_file(dist_name=dist_name)
except LookupError as e:
- trace(e)
+ log.exception(e)
else:
_assign_version(dist, config)
+
+
+def data_from_mime(path: _t.PathT) -> dict[str, str]:
+ content = Path(path).read_text(encoding="utf-8")
+ log.debug("mime %s content:\n%s", path, textwrap.indent(content, " "))
+ # the complex conditions come from reading pseudo-mime-messages
+ data = dict(x.split(": ", 1) for x in content.splitlines() if ": " in x)
+
+ log.debug("mime %s data:\n%s", path, data)
+ return data
diff --git a/src/setuptools_scm/scm_workdir.py b/src/setuptools_scm/scm_workdir.py
index 113f68a..9879549 100644
--- a/src/setuptools_scm/scm_workdir.py
+++ b/src/setuptools_scm/scm_workdir.py
@@ -1,26 +1,15 @@
from __future__ import annotations
-from typing import ClassVar
-from typing import TYPE_CHECKING
+from dataclasses import dataclass
+from pathlib import Path
-from .utils import _CmdResult
-from .utils import do
-from .utils import do_ex
-from .utils import require_command
-
-if TYPE_CHECKING:
- from . import _types as _t
+from ._config import Configuration
+from .version import ScmVersion
+@dataclass()
class Workdir:
- COMMAND: ClassVar[str]
-
- def __init__(self, path: _t.PathT):
- require_command(self.COMMAND)
- self.path = path
-
- def do_ex(self, cmd: _t.CMD_TYPE) -> _CmdResult:
- return do_ex(cmd, cwd=self.path)
+ path: Path
- def do(self, cmd: _t.CMD_TYPE) -> str:
- return do(cmd, cwd=self.path)
+ def run_describe(self, config: Configuration) -> ScmVersion:
+ raise NotImplementedError(self.run_describe)
diff --git a/src/setuptools_scm/utils.py b/src/setuptools_scm/utils.py
deleted file mode 100644
index 6788f23..0000000
--- a/src/setuptools_scm/utils.py
+++ /dev/null
@@ -1,79 +0,0 @@
-"""
-utils
-"""
-from __future__ import annotations
-
-import logging
-import subprocess
-import sys
-import warnings
-from types import CodeType
-from types import FunctionType
-from typing import NamedTuple
-from typing import TYPE_CHECKING
-
-from . import _run_cmd
-from . import _trace
-
-if TYPE_CHECKING:
- from . import _types as _t
-
-log = logging.getLogger(__name__)
-
-
-class _CmdResult(NamedTuple):
- out: str
- err: str
- returncode: int
-
-
-def do_ex(cmd: _t.CMD_TYPE, cwd: _t.PathT = ".") -> _CmdResult:
- res = _run_cmd.run(cmd, cwd)
- return _CmdResult(res.stdout, res.stderr, res.returncode)
-
-
-def do(cmd: _t.CMD_TYPE, cwd: _t.PathT = ".") -> str:
- out, err, ret = do_ex(cmd, cwd)
- if ret and not _trace.DEBUG:
- print(err)
- return out
-
-
-def data_from_mime(path: _t.PathT) -> dict[str, str]:
- with open(path, encoding="utf-8") as fp:
- content = fp.read()
- _trace.trace("content", repr(content))
- # the complex conditions come from reading pseudo-mime-messages
- data = dict(x.split(": ", 1) for x in content.splitlines() if ": " in x)
- _trace.trace("data", data)
- return data
-
-
-def function_has_arg(fn: object | FunctionType, argname: str) -> bool:
- assert isinstance(fn, FunctionType)
- code: CodeType = fn.__code__
- return argname in code.co_varnames
-
-
-def has_command(name: str, args: list[str] | None = None, warn: bool = True) -> bool:
- try:
- cmd = [name, "help"] if args is None else [name, *args]
- p = _run_cmd.run(cmd, cwd=".", timeout=5)
- except OSError:
- _trace.trace(*sys.exc_info())
- res = False
- except subprocess.TimeoutExpired as e:
- log.info(e)
- _trace.trace(e)
- res = False
-
- else:
- res = not p.returncode
- if not res and warn:
- warnings.warn("%r was not found" % name, category=RuntimeWarning)
- return res
-
-
-def require_command(name: str) -> None:
- if not has_command(name, warn=False):
- raise OSError(f"{name!r} was not found")
diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py
index 36bc7df..128533c 100644
--- a/src/setuptools_scm/version.py
+++ b/src/setuptools_scm/version.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import dataclasses
+import logging
import os
import re
import warnings
@@ -13,22 +14,21 @@ from typing import Match
from typing import TYPE_CHECKING
from . import _entrypoints
-from ._modify_version import _bump_dev
-from ._modify_version import _bump_regex
-from ._modify_version import _dont_guess_next_version
-from ._modify_version import _format_local_with_time
-from ._modify_version import _strip_local
+from . import _modify_version
if TYPE_CHECKING:
from typing_extensions import Concatenate
+ from typing_extensions import ParamSpec
- from . import _types as _t
+ _P = ParamSpec("_P")
from ._version_cls import Version as PkgVersion, _VersionT
from . import _version_cls as _v
from . import _config
-from ._trace import trace
+
+log = logging.getLogger(__name__)
+
SEMVER_MINOR = 2
SEMVER_PATCH = 3
@@ -55,19 +55,19 @@ def _parse_version_tag(
"suffix": match.group(0)[match.end(key) :],
}
- trace(f"tag '{tag}' parsed to {result}")
+ log.debug(f"tag '{tag}' parsed to {result}")
return result
def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any:
- trace("ep", (group, callable_or_name))
+ log.debug("ep %r %r", group, callable_or_name)
if callable(callable_or_name):
return callable_or_name
from ._entrypoints import iter_entry_points
for ep in iter_entry_points(group, callable_or_name):
- trace("ep found:", ep.name)
+ log.debug("ep found: %s", ep.name)
return ep.load()
@@ -77,7 +77,7 @@ def tag_to_version(
"""
take a tag that might be prefixed with a keyword and return only the version part
"""
- trace("tag", tag)
+ log.debug("tag %s", tag)
tagdict = _parse_version_tag(tag, config)
if not isinstance(tagdict, dict) or not tagdict.get("version", None):
@@ -85,7 +85,7 @@ def tag_to_version(
return None
version_str = tagdict["version"]
- trace("version pre parse", version_str)
+ log.debug("version pre parse %s", version_str)
if tagdict.get("suffix", ""):
warnings.warn(
@@ -95,7 +95,7 @@ def tag_to_version(
)
version: _VersionT = config.version_cls(version_str)
- trace("version", repr(version))
+ log.debug("version=%r", version)
return version
@@ -112,7 +112,7 @@ def _source_epoch_or_utc_now() -> datetime:
class ScmVersion:
tag: _v.Version | _v.NonNormalizedVersion | str
config: _config.Configuration
- distance: int | None = None
+ distance: int = 0
node: str | None = None
dirty: bool = False
preformatted: bool = False
@@ -122,13 +122,9 @@ class ScmVersion:
init=False, default_factory=_source_epoch_or_utc_now
)
- def __post_init__(self) -> None:
- if self.dirty and self.distance is None:
- self.distance = 0
-
@property
def exact(self) -> bool:
- return self.distance is None
+ return self.distance == 0 and not self.dirty
def __repr__(self) -> str:
return self.format_with(
@@ -153,10 +149,10 @@ class ScmVersion:
def format_next_version(
self,
- guess_next: Callable[Concatenate[ScmVersion, _t.P], str],
+ guess_next: Callable[Concatenate[ScmVersion, _P], str],
fmt: str = "{guessed}.dev{distance}",
- *k: _t.P.args,
- **kw: _t.P.kwargs,
+ *k: _P.args,
+ **kw: _P.kwargs,
) -> str:
guessed = guess_next(self, *k, **kw)
return self.format_with(fmt, guessed=guessed)
@@ -178,7 +174,7 @@ def _parse_tag(
def meta(
tag: str | _VersionT,
*,
- distance: int | None = None,
+ distance: int = 0,
dirty: bool = False,
node: str | None = None,
preformatted: bool = False,
@@ -187,7 +183,7 @@ def meta(
node_date: date | None = None,
) -> ScmVersion:
parsed_version = _parse_tag(tag, preformatted, config)
- trace("version", tag, "->", parsed_version)
+ log.info("version %s -> %s", tag, parsed_version)
assert parsed_version is not None, "Can't parse version %s" % tag
return ScmVersion(
parsed_version,
@@ -202,8 +198,8 @@ def meta(
def guess_next_version(tag_version: ScmVersion) -> str:
- version = _strip_local(str(tag_version.tag))
- return _bump_dev(version) or _bump_regex(version)
+ version = _modify_version._strip_local(str(tag_version.tag))
+ return _modify_version._bump_dev(version) or _modify_version._bump_regex(version)
def guess_next_dev_version(version: ScmVersion) -> str:
@@ -282,7 +278,7 @@ def no_guess_dev_version(version: ScmVersion) -> str:
if version.exact:
return version.format_with("{tag}")
else:
- return version.format_next_version(_dont_guess_next_version)
+ return version.format_next_version(_modify_version._dont_guess_next_version)
_DATE_REGEX = re.compile(
@@ -366,11 +362,11 @@ def calver_by_date(version: ScmVersion) -> str:
def get_local_node_and_date(version: ScmVersion) -> str:
- return _format_local_with_time(version, time_format="%Y%m%d")
+ return _modify_version._format_local_with_time(version, time_format="%Y%m%d")
def get_local_node_and_timestamp(version: ScmVersion, fmt: str = "%Y%m%d%H%M%S") -> str:
- return _format_local_with_time(version, time_format=fmt)
+ return _modify_version._format_local_with_time(version, time_format=fmt)
def get_local_dirty_tag(version: ScmVersion) -> str:
@@ -389,18 +385,18 @@ def postrelease_version(version: ScmVersion) -> str:
def format_version(version: ScmVersion, **config: Any) -> str:
- trace("scm version", version)
- trace("config", config)
+ log.debug("scm version %s", version)
+ log.debug("config %s", config)
if version.preformatted:
assert isinstance(version.tag, str)
return version.tag
main_version = _entrypoints._call_version_scheme(
version, "setuptools_scm.version_scheme", config["version_scheme"], None
)
- trace("version", main_version)
+ log.debug("version %s", main_version)
assert main_version is not None
local_version = _entrypoints._call_version_scheme(
version, "setuptools_scm.local_scheme", config["local_scheme"], "+unknown"
)
- trace("local_version", local_version)
+ log.debug("local_version %s", local_version)
return main_version + local_version
diff --git a/testing/conftest.py b/testing/conftest.py
index e1c8160..ef5883c 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -1,12 +1,14 @@
from __future__ import annotations
+import contextlib
import os
from pathlib import Path
+from types import TracebackType
from typing import Any
+from typing import Iterator
import pytest
-import setuptools_scm.utils
from .wd_wrapper import WorkDir
from setuptools_scm._run_cmd import run
@@ -40,25 +42,35 @@ def pytest_addoption(parser: Any) -> None:
)
-class DebugMode:
- def __init__(self, monkeypatch: pytest.MonkeyPatch):
- self.__monkeypatch = monkeypatch
- self.__module = setuptools_scm._trace
+class DebugMode(contextlib.AbstractContextManager): # type: ignore[type-arg]
+ from setuptools_scm import _log as __module
- __monkeypatch: pytest.MonkeyPatch
+ def __init__(self) -> None:
+ self.__stack = contextlib.ExitStack()
+
+ def __enter__(self) -> DebugMode:
+ self.enable()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.disable()
def enable(self) -> None:
- self.__monkeypatch.setattr(self.__module, "DEBUG", True)
+ self.__stack.enter_context(self.__module.defer_to_pytest())
def disable(self) -> None:
- self.__monkeypatch.setattr(self.__module, "DEBUG", False)
+ self.__stack.close()
@pytest.fixture(autouse=True)
-def debug_mode(monkeypatch: pytest.MonkeyPatch) -> DebugMode:
- debug_mode = DebugMode(monkeypatch)
- debug_mode.enable()
- return debug_mode
+def debug_mode() -> Iterator[DebugMode]:
+ with DebugMode() as debug_mode:
+ yield debug_mode
@pytest.fixture
diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py
index bf21a47..f1b4c4f 100644
--- a/testing/test_basic_api.py
+++ b/testing/test_basic_api.py
@@ -1,7 +1,6 @@
from __future__ import annotations
import os
-import shutil
import sys
from pathlib import Path
@@ -11,17 +10,13 @@ import setuptools_scm
from setuptools_scm import Configuration
from setuptools_scm import dump_version
from setuptools_scm._run_cmd import run
-from setuptools_scm.utils import data_from_mime
-from setuptools_scm.utils import do
+from setuptools_scm.integration import data_from_mime
from setuptools_scm.version import ScmVersion
from testing.wd_wrapper import WorkDir
-@pytest.mark.parametrize("cmd", ["ls", "dir"])
-def test_do(cmd: str, tmp_path: Path) -> None:
- if not shutil.which(cmd):
- pytest.skip(f"{cmd} not found")
- do(cmd, cwd=tmp_path)
+def test_run_plain(tmp_path: Path) -> None:
+ run([sys.executable, "-c", "print(1)"], cwd=tmp_path)
def test_data_from_mime(tmp_path: Path) -> None:
@@ -35,7 +30,7 @@ def test_data_from_mime(tmp_path: Path) -> None:
def test_version_from_pkginfo(wd: WorkDir) -> None:
wd.write("PKG-INFO", "Version: 0.1")
- assert wd.version == "0.1"
+ assert wd.get_version() == "0.1"
# replicate issue 167
assert wd.get_version(version_scheme="1.{0.distance}.0".format) == "0.1"
diff --git a/testing/test_functions.py b/testing/test_functions.py
index ceb6cd3..6b0bd39 100644
--- a/testing/test_functions.py
+++ b/testing/test_functions.py
@@ -8,7 +8,7 @@ from setuptools_scm import Configuration
from setuptools_scm import dump_version
from setuptools_scm import get_version
from setuptools_scm import PRETEND_KEY
-from setuptools_scm.utils import has_command
+from setuptools_scm._run_cmd import has_command
from setuptools_scm.version import format_version
from setuptools_scm.version import guess_next_version
from setuptools_scm.version import meta
@@ -38,36 +38,37 @@ def test_next_tag(tag: str, expected: str) -> None:
VERSIONS = {
- "exact": meta("1.1", distance=None, dirty=False, config=c),
- "zerodistance": meta("1.1", distance=0, dirty=False, config=c),
- "dirty": meta("1.1", distance=None, dirty=True, config=c),
- "distance": meta("1.1", distance=3, dirty=False, config=c),
- "distancedirty": meta("1.1", distance=3, dirty=True, config=c),
+ "exact": meta("1.1", distance=0, dirty=False, config=c),
+ "dirty": meta("1.1", distance=0, dirty=True, config=c),
+ "distance-clean": meta("1.1", distance=3, dirty=False, config=c),
+ "distance-dirty": meta("1.1", distance=3, dirty=True, config=c),
}
@pytest.mark.parametrize(
- "version,scheme,expected",
+ "version,version_scheme, local_scheme,expected",
[
- ("exact", "guess-next-dev node-and-date", "1.1"),
- ("zerodistance", "guess-next-dev node-and-date", "1.2.dev0"),
- ("zerodistance", "guess-next-dev no-local-version", "1.2.dev0"),
- ("dirty", "guess-next-dev node-and-date", "1.2.dev0+d20090213"),
- ("dirty", "guess-next-dev no-local-version", "1.2.dev0"),
- ("distance", "guess-next-dev node-and-date", "1.2.dev3"),
- ("distancedirty", "guess-next-dev node-and-date", "1.2.dev3+d20090213"),
- ("distancedirty", "guess-next-dev no-local-version", "1.2.dev3"),
- ("exact", "post-release node-and-date", "1.1"),
- ("zerodistance", "post-release node-and-date", "1.1.post0"),
- ("dirty", "post-release node-and-date", "1.1.post0+d20090213"),
- ("distance", "post-release node-and-date", "1.1.post3"),
- ("distancedirty", "post-release node-and-date", "1.1.post3+d20090213"),
+ ("exact", "guess-next-dev", "node-and-date", "1.1"),
+ ("dirty", "guess-next-dev", "node-and-date", "1.2.dev0+d20090213"),
+ ("dirty", "guess-next-dev", "no-local-version", "1.2.dev0"),
+ ("distance-clean", "guess-next-dev", "node-and-date", "1.2.dev3"),
+ ("distance-dirty", "guess-next-dev", "node-and-date", "1.2.dev3+d20090213"),
+ ("exact", "post-release", "node-and-date", "1.1"),
+ ("dirty", "post-release", "node-and-date", "1.1.post0+d20090213"),
+ ("distance-clean", "post-release", "node-and-date", "1.1.post3"),
+ ("distance-dirty", "post-release", "node-and-date", "1.1.post3+d20090213"),
],
)
-def test_format_version(version: str, scheme: str, expected: str) -> None:
+def test_format_version(
+ version: str, version_scheme: str, local_scheme: str, expected: str
+) -> None:
scm_version = VERSIONS[version]
- vs, ls = scheme.split()
- assert format_version(scm_version, version_scheme=vs, local_scheme=ls) == expected
+ assert (
+ format_version(
+ scm_version, version_scheme=version_scheme, local_scheme=local_scheme
+ )
+ == expected
+ )
def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None:
diff --git a/testing/test_git.py b/testing/test_git.py
index 3e1ee67..9e2e9b5 100644
--- a/testing/test_git.py
+++ b/testing/test_git.py
@@ -22,9 +22,10 @@ from setuptools_scm import Configuration
from setuptools_scm import git
from setuptools_scm import NonNormalizedVersion
from setuptools_scm._file_finders.git import git_find_files
+from setuptools_scm._run_cmd import CompletedProcess
+from setuptools_scm._run_cmd import has_command
from setuptools_scm._run_cmd import run
from setuptools_scm.git import archival_to_version
-from setuptools_scm.utils import has_command
from setuptools_scm.version import format_version
pytestmark = pytest.mark.skipif(
@@ -70,7 +71,7 @@ setup(use_scm_version={"root": "../..",
"""
)
res = run([sys.executable, "setup.py", "--version"], p)
- assert res.stdout == "0.1.dev0"
+ assert res.stdout == "0.1.dev0+d20090213"
def test_root_search_parent_directories(
@@ -85,7 +86,7 @@ setup(use_scm_version={"search_parent_directories": True})
"""
)
res = run([sys.executable, "setup.py", "--version"], p)
- assert res.stdout == "0.1.dev0"
+ assert res.stdout == "0.1.dev0+d20090213"
def test_git_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -146,37 +147,37 @@ def test_not_owner(wd: WorkDir) -> None:
def test_version_from_git(wd: WorkDir) -> None:
- assert wd.version == "0.1.dev0"
+ assert wd.get_version() == "0.1.dev0+d20090213"
parsed = git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE)
assert parsed is not None and parsed.branch in ("master", "main")
wd.commit_testfile()
- assert wd.version.startswith("0.1.dev1+g")
- assert not wd.version.endswith("1-")
+ assert wd.get_version().startswith("0.1.dev1+g")
+ assert not wd.get_version().endswith("1-")
wd("git tag v0.1")
- assert wd.version == "0.1"
+ assert wd.get_version() == "0.1"
wd.write("test.txt", "test2")
- assert wd.version.startswith("0.2.dev0+g")
+ assert wd.get_version().startswith("0.2.dev0+g")
wd.commit_testfile()
- assert wd.version.startswith("0.2.dev1+g")
+ assert wd.get_version().startswith("0.2.dev1+g")
wd("git tag version-0.2")
- assert wd.version.startswith("0.2")
+ assert wd.get_version().startswith("0.2")
wd.commit_testfile()
wd("git tag version-0.2.post210+gbe48adfpost3+g0cc25f2")
with pytest.warns(
UserWarning, match="tag '.*' will be stripped of its suffix '.*'"
):
- assert wd.version.startswith("0.2")
+ assert wd.get_version().startswith("0.2")
wd.commit_testfile()
wd("git tag 17.33.0-rc")
- assert wd.version == "17.33.0rc0"
+ assert wd.get_version() == "17.33.0rc0"
# custom normalization
assert wd.get_version(normalize=False) == "17.33.0-rc"
@@ -253,9 +254,10 @@ def test_unicode_version_scheme(wd: WorkDir) -> None:
def test_git_worktree(wd: WorkDir) -> None:
wd.write("test.txt", "test2")
# untracked files dont change the state
- assert wd.version == "0.1.dev0"
+ assert wd.get_version() == "0.1.dev0+d20090213"
+
wd("git add test.txt")
- assert wd.version.startswith("0.1.dev0+d")
+ assert wd.get_version().startswith("0.1.dev0+d")
@pytest.mark.issue(86)
@@ -268,14 +270,14 @@ def test_git_dirty_notag(
wd.commit_testfile()
wd.write("test.txt", "test2")
wd("git add test.txt")
- assert wd.version.startswith("0.1.dev1")
+ version = wd.get_version()
+
if today:
# the date on the tag is in UTC
tag = datetime.now(timezone.utc).date().strftime(".d%Y%m%d")
else:
tag = ".d20090213"
- # we are dirty, check for the tag
- assert tag in wd.version
+ assert version.startswith("0.1.dev1+g") and version.endswith(tag)
@pytest.mark.issue(193)
@@ -303,7 +305,8 @@ def shallow_wd(wd: WorkDir, tmp_path: Path) -> Path:
def test_git_parse_shallow_warns(
shallow_wd: Path, recwarn: pytest.WarningsRecorder
) -> None:
- git.parse(str(shallow_wd), Configuration())
+ git.parse(shallow_wd, Configuration())
+ print(list(recwarn))
msg = recwarn.pop()
assert "is shallow and may cause errors" in str(msg.message)
@@ -339,7 +342,7 @@ def test_parse_no_worktree(tmp_path: Path) -> None:
def test_alphanumeric_tags_match(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git tag newstyle-development-started")
- assert wd.version.startswith("0.1.dev1+g")
+ assert wd.get_version().startswith("0.1.dev1+g")
def test_git_archive_export_ignore(
@@ -387,7 +390,7 @@ def test_git_archive_run_from_subdirectory(
def test_git_branch_names_correct(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git checkout -b test/fun")
- wd_git = git.GitWorkdir(os.fspath(wd.cwd))
+ wd_git = git.GitWorkdir(wd.cwd)
assert wd_git.get_branch() == "test/fun"
@@ -443,10 +446,10 @@ def test_non_dotted_tag_no_version_match(wd: WorkDir) -> None:
def test_gitdir(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
""" """
wd.commit_testfile()
- normal = wd.version
+ normal = wd.get_version()
# git hooks set this and break subsequent setuptools_scm unless we clean
monkeypatch.setenv("GIT_DIR", __file__)
- assert wd.version == normal
+ assert wd.get_version() == normal
def test_git_getdate(wd: WorkDir) -> None:
@@ -459,7 +462,7 @@ def test_git_getdate(wd: WorkDir) -> None:
assert parsed.node_date is not None
return parsed.node_date
- git_wd = git.GitWorkdir(os.fspath(wd.cwd))
+ git_wd = git.GitWorkdir(wd.cwd)
assert git_wd.get_head_date() is None
assert parse_date() == today
@@ -468,10 +471,17 @@ def test_git_getdate(wd: WorkDir) -> None:
assert parse_date() == today
-def test_git_getdate_badgit(wd: WorkDir) -> None:
+def test_git_getdate_badgit(
+ wd: WorkDir, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
+) -> None:
wd.commit_testfile()
- git_wd = git.GitWorkdir(os.fspath(wd.cwd))
- with patch.object(git_wd, "do_ex", Mock(return_value=("%cI", "", 0))):
+ git_wd = git.GitWorkdir(wd.cwd)
+ fake_date_result = CompletedProcess(args=[], stdout="%cI", stderr="", returncode=0)
+ with patch.object(
+ git,
+ "run_git",
+ Mock(return_value=fake_date_result),
+ ):
assert git_wd.get_head_date() is None
@@ -504,7 +514,7 @@ Expire-Date: 0
def test_git_getdate_signed_commit(signed_commit_wd: WorkDir) -> None:
today = date.today()
signed_commit_wd.commit_testfile(signed=True)
- git_wd = git.GitWorkdir(os.fspath(signed_commit_wd.cwd))
+ git_wd = git.GitWorkdir(signed_commit_wd.cwd)
assert git_wd.get_head_date() == today
@@ -551,6 +561,8 @@ def test_git_archival_node_missing_no_version() -> None:
def test_git_archival_from_unfiltered() -> None:
config = Configuration()
- with pytest.warns(UserWarning, match="unexported git archival found"):
+ with pytest.warns(
+ UserWarning, match=r"unprocessed git archival found \(no export subst applied\)"
+ ):
version = archival_to_version({"node": "$Format:%H$"}, config=config)
assert version is None
diff --git a/testing/test_hg_git.py b/testing/test_hg_git.py
index 476016f..9527cb0 100644
--- a/testing/test_hg_git.py
+++ b/testing/test_hg_git.py
@@ -2,8 +2,8 @@ from __future__ import annotations
import pytest
-from setuptools_scm.utils import do_ex
-from setuptools_scm.utils import has_command
+from setuptools_scm._run_cmd import has_command
+from setuptools_scm._run_cmd import run
from testing.wd_wrapper import WorkDir
@@ -12,13 +12,13 @@ def _check_hg_git() -> None:
if not has_command("hg", warn=False):
pytest.skip("hg executable not found")
- python_hg, err, ret = do_ex("hg debuginstall --template {pythonexe}")
+ res = run("hg debuginstall --template {pythonexe}", cwd=".")
- if ret:
+ if res.returncode:
skip_no_hggit = True
else:
- out, err, ret = do_ex([python_hg.strip(), "-c", "import hggit"])
- skip_no_hggit = bool(ret)
+ res = run([res.stdout, "-c", "import hggit"], cwd=".")
+ skip_no_hggit = bool(res.returncode)
if skip_no_hggit:
pytest.skip("hg-git not installed")
@@ -26,15 +26,15 @@ def _check_hg_git() -> None:
def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None:
wd, wd_git = repositories_hg_git
- assert wd_git.version == "0.1.dev0"
- assert wd.version == "0.1.dev0"
+ assert wd_git.get_version() == "0.1.dev0+d20090213"
+ assert wd.get_version() == "0.1.dev0+d20090213"
wd_git.commit_testfile()
- version_git = wd_git.version
+ version_git = wd_git.get_version()
wd("hg pull -u")
- version = wd.version
+ version = wd.get_version()
assert version_git.startswith("0.1.dev1+g")
assert version.startswith("0.1.dev1+g")
@@ -44,24 +44,24 @@ def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None:
wd_git("git tag v0.1")
wd("hg pull -u")
- assert wd_git.version == "0.1"
- assert wd.version == "0.1"
+ assert wd_git.get_version() == "0.1"
+ assert wd.get_version() == "0.1"
wd_git.write("test.txt", "test2")
wd.write("test.txt", "test2")
- assert wd_git.version.startswith("0.2.dev0+g")
- assert wd.version.startswith("0.2.dev0+g")
+ assert wd_git.get_version().startswith("0.2.dev0+g")
+ assert wd.get_version().startswith("0.2.dev0+g")
wd_git.commit_testfile()
wd("hg pull")
wd("hg up -C")
- assert wd_git.version.startswith("0.2.dev1+g")
- assert wd.version.startswith("0.2.dev1+g")
+ assert wd_git.get_version().startswith("0.2.dev1+g")
+ assert wd.get_version().startswith("0.2.dev1+g")
wd_git("git tag version-0.2")
wd("hg pull -u")
- assert wd_git.version.startswith("0.2")
- assert wd.version.startswith("0.2")
+ assert wd_git.get_version().startswith("0.2")
+ assert wd.get_version().startswith("0.2")
wd_git.commit_testfile()
wd_git("git tag version-0.2.post210+gbe48adfpost3+g0cc25f2")
@@ -69,15 +69,15 @@ def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None:
with pytest.warns(
UserWarning, match="tag '.*' will be stripped of its suffix '.*'"
):
- assert wd_git.version.startswith("0.2")
+ assert wd_git.get_version().startswith("0.2")
with pytest.warns(
UserWarning, match="tag '.*' will be stripped of its suffix '.*'"
):
- assert wd.version.startswith("0.2")
+ assert wd.get_version().startswith("0.2")
wd_git.commit_testfile()
wd_git("git tag 17.33.0-rc")
wd("hg pull -u")
- assert wd_git.version == "17.33.0rc0"
- assert wd.version == "17.33.0rc0"
+ assert wd_git.get_version() == "17.33.0rc0"
+ assert wd.get_version() == "17.33.0rc0"
diff --git a/testing/test_integration.py b/testing/test_integration.py
index cba0f98..c0594ad 100644
--- a/testing/test_integration.py
+++ b/testing/test_integration.py
@@ -91,7 +91,7 @@ def test_pyproject_support_with_git(wd: WorkDir, metadata_in: str) -> None:
wd.write("setup.py", SETUP_PY_FILES[metadata_in])
wd.write("setup.cfg", SETUP_CFG_FILES[metadata_in])
res = wd([sys.executable, "setup.py", "--version"])
- assert res.endswith("0.1.dev0")
+ assert res.endswith("0.1.dev0+d20090213")
def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
@@ -152,7 +152,7 @@ def test_distribution_provides_extras() -> None:
from importlib_metadata import distribution
dist = distribution("setuptools_scm")
- assert sorted(dist.metadata.get_all("Provides-Extra")) == ["test", "toml"]
+ assert sorted(dist.metadata.get_all("Provides-Extra")) == ["rich", "test", "toml"]
@pytest.mark.issue(760)
diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py
index 7de7ca2..1b35d11 100644
--- a/testing/test_mercurial.py
+++ b/testing/test_mercurial.py
@@ -7,9 +7,9 @@ import pytest
import setuptools_scm._file_finders
from setuptools_scm import Configuration
+from setuptools_scm._run_cmd import has_command
from setuptools_scm.hg import archival_to_version
from setuptools_scm.hg import parse
-from setuptools_scm.utils import has_command
from setuptools_scm.version import format_version
from testing.wd_wrapper import WorkDir
@@ -76,31 +76,31 @@ def test_find_files_stop_at_root_hg(
# XXX: better tests for tag prefixes
def test_version_from_hg_id(wd: WorkDir) -> None:
- assert wd.version == "0.0"
+ assert wd.get_version() == "0.0"
wd.commit_testfile()
- assert wd.version.startswith("0.1.dev1+")
+ assert wd.get_version().startswith("0.1.dev1+")
# tagging commit is considered the tag
wd('hg tag v0.1 -u test -d "0 0"')
- assert wd.version == "0.1"
+ assert wd.get_version() == "0.1"
wd.commit_testfile()
- assert wd.version.startswith("0.2.dev2")
+ assert wd.get_version().startswith("0.2.dev2")
wd("hg up v0.1")
- assert wd.version == "0.1"
+ assert wd.get_version() == "0.1"
# commit originating from the tagged revision
- # that is not a actual tag
+ # that is not an actual tag
wd.commit_testfile()
- assert wd.version.startswith("0.2.dev1+")
+ assert wd.get_version().startswith("0.2.dev1+")
# several tags
wd("hg up")
wd('hg tag v0.2 -u test -d "0 0"')
wd('hg tag v0.3 -u test -d "0 0" -r v0.2')
- assert wd.version == "0.3"
+ assert wd.get_version() == "0.3"
def test_version_from_archival(wd: WorkDir) -> None:
@@ -108,14 +108,14 @@ def test_version_from_archival(wd: WorkDir) -> None:
# cleaning the wd ensure this test won't break randomly
wd.cwd.joinpath(".hg").rename(wd.cwd / ".nothg")
wd.write(".hg_archival.txt", "node: 000000000000\n" "tag: 0.1\n")
- assert wd.version == "0.1"
+ assert wd.get_version() == "0.1"
wd.write(
".hg_archival.txt",
"node: 000000000000\n" "latesttag: 0.1\n" "latesttagdistance: 3\n",
)
- assert wd.version == "0.2.dev3+h000000000000"
+ assert wd.get_version() == "0.2.dev3+h000000000000"
@pytest.mark.issue("#72")
@@ -125,7 +125,7 @@ def test_version_in_merge(wd: WorkDir) -> None:
wd("hg up 0")
wd.commit_testfile()
wd("hg merge --tool :merge")
- assert wd.version is not None
+ assert wd.get_version() is not None
@pytest.mark.issue(128)
@@ -157,14 +157,14 @@ def pre_merge_commit_after_tag(version_1_0: WorkDir) -> WorkDir:
@pytest.mark.usefixtures("pre_merge_commit_after_tag")
def test_version_bump_before_merge_commit(wd: WorkDir) -> None:
- assert wd.version.startswith("1.0.1.dev1+")
+ assert wd.get_version().startswith("1.0.1.dev1+")
@pytest.mark.issue(219)
@pytest.mark.usefixtures("pre_merge_commit_after_tag")
def test_version_bump_from_merge_commit(wd: WorkDir) -> None:
wd.commit()
- assert wd.version.startswith("1.0.1.dev3+") # issue 219
+ assert wd.get_version().startswith("1.0.1.dev3+") # issue 219
@pytest.mark.usefixtures("version_1_0")
@@ -174,9 +174,9 @@ def test_version_bump_from_commit_including_hgtag_mods(wd: WorkDir) -> None:
tagfile.write(b"0 0\n")
wd.write("branchfile", "branchtext")
wd(wd.add_command)
- assert wd.version.startswith("1.0.1.dev1+") # bump from dirty version
+ assert wd.get_version().startswith("1.0.1.dev1+") # bump from dirty version
wd.commit() # commits both the testfile _and_ .hgtags
- assert wd.version.startswith("1.0.1.dev2+")
+ assert wd.get_version().startswith("1.0.1.dev2+")
@pytest.mark.issue(229)
@@ -186,7 +186,7 @@ def test_latest_tag_detection(wd: WorkDir) -> None:
Note that will be superseded by the fix for pypa/setuptools_scm/issues/235
"""
wd('hg tag some-random-tag -u test -d "0 0"')
- assert wd.version == "1.0.0"
+ assert wd.get_version() == "1.0.0"
@pytest.mark.usefixtures("version_1_0")
diff --git a/testing/test_regressions.py b/testing/test_regressions.py
index f7b4b17..018f37e 100644
--- a/testing/test_regressions.py
+++ b/testing/test_regressions.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import os
import pprint
import subprocess
import sys
@@ -16,7 +15,6 @@ from pathlib import Path
import pytest
from setuptools_scm import Configuration
-from setuptools_scm import get_version
from setuptools_scm.git import parse
from setuptools_scm._run_cmd import run
@@ -48,28 +46,7 @@ def test_pkginfo_noscmroot(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> N
pass
else:
res = run([sys.executable, "setup.py", "--version"], p)
- assert res.stdout == "0.1.dev0"
-
-
-def test_pip_egg_info(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
- """if we are indeed a sdist, the root does not apply"""
-
- # we should get the version from pkg-info if git is broken
- p = tmp_path.joinpath("sub/package")
- p.mkdir(parents=True)
- tmp_path.joinpath(".git").mkdir()
- p.joinpath("setup.py").write_text(
- "from setuptools import setup;" 'setup(use_scm_version={"root": ".."})'
- )
-
- with pytest.raises(LookupError):
- get_version(root=os.fspath(p), fallback_root=os.fspath(p))
-
- bad_egg_info = p.joinpath("pip-egg-info/random.egg-info/")
- bad_egg_info.mkdir(parents=True)
-
- bad_egg_info.joinpath("PKG-INFO").write_text("Version: 1.0")
- assert get_version(root=os.fspath(p), fallback_root=os.fspath(p)) == "1.0"
+ assert res.stdout == "0.1.dev0+d20090213"
@pytest.mark.issue(164)
diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py
index 1587f82..dd504ee 100644
--- a/testing/wd_wrapper.py
+++ b/testing/wd_wrapper.py
@@ -23,15 +23,12 @@ class WorkDir:
if kw:
assert isinstance(cmd, str), "formatting the command requires text input"
cmd = cmd.format(**kw)
- from setuptools_scm.utils import do
+ from setuptools_scm._run_cmd import run
- return do(cmd, self.cwd)
+ return run(cmd, cwd=self.cwd).stdout
- def write(self, name: str, content: str | bytes, **kw: object) -> Path:
+ def write(self, name: str, content: str | bytes) -> Path:
path = self.cwd / name
- if kw:
- assert isinstance(content, str)
- content = content.format(**kw)
if isinstance(content, bytes):
path.write_bytes(content)
else:
@@ -59,7 +56,7 @@ class WorkDir:
def commit_testfile(self, reason: str | None = None, signed: bool = False) -> None:
reason = self._reason(reason)
- self.write("test.txt", "test {reason}", reason=reason)
+ self.write("test.txt", f"test {reason}")
self(self.add_command)
self.commit(reason=reason, signed=signed)
@@ -68,10 +65,5 @@ class WorkDir:
from setuptools_scm import get_version
version = get_version(root=self.cwd, fallback_root=self.cwd, **kw)
- print(version)
+ print(self.cwd.name, version, sep=": ")
return version
-
- @property
- def version(self) -> str:
- __tracebackhide__ = True
- return self.get_version()
diff --git a/tox.ini b/tox.ini
index b183b09..b7c0123 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,6 +7,8 @@ filterwarnings=
error
ignore:.*tool\.setuptools_scm.*
ignore:.*git archive did not support describe output.*:UserWarning
+log_level = debug
+log_cli_level = info
markers=
issue(id): reference to github issue
skip_commit: allows to skip committing in the helpers
@@ -37,8 +39,8 @@ deps=
check-manifest
docutils
pygments
- setuptools>45
typing_extensions
+ hatchling
commands=
rst2html.py README.rst {envlogdir}/README.html --strict []
check-manifest --no-build-isolation