From ff7693bf4b49d99e76881095dc6bb75683da66c0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Sep 2023 21:18:05 +0200 Subject: [PATCH 1/3] introduce CommandNotFoundError for require_command --- src/setuptools_scm/_run_cmd.py | 6 +++++- testing/test_git.py | 5 +++-- testing/test_mercurial.py | 5 +++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/setuptools_scm/_run_cmd.py b/src/setuptools_scm/_run_cmd.py index b1e5f1ab..20dbb25f 100644 --- a/src/setuptools_scm/_run_cmd.py +++ b/src/setuptools_scm/_run_cmd.py @@ -189,6 +189,10 @@ def has_command( return res +class CommandNotFoundError(LookupError, FileNotFoundError): + pass + + def require_command(name: str) -> None: if not has_command(name, warn=False): - raise OSError(f"{name!r} was not found") + raise CommandNotFoundError(name) diff --git a/testing/test_git.py b/testing/test_git.py index cec9afa4..3642ee1a 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -24,6 +24,7 @@ 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 CommandNotFoundError from setuptools_scm._run_cmd import CompletedProcess from setuptools_scm._run_cmd import has_command from setuptools_scm._run_cmd import run @@ -93,8 +94,8 @@ def test_root_search_parent_directories( def test_git_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) - with pytest.raises(EnvironmentError, match="'git' was not found"): - git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE) + with pytest.raises(CommandNotFoundError, match=r"git"): + git.parse(wd.cwd, Configuration(), git.DEFAULT_DESCRIBE) @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/298") diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 1b35d11c..ea50ece5 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -7,6 +7,7 @@ import setuptools_scm._file_finders from setuptools_scm import Configuration +from setuptools_scm._run_cmd import CommandNotFoundError from setuptools_scm._run_cmd import has_command from setuptools_scm.hg import archival_to_version from setuptools_scm.hg import parse @@ -55,8 +56,8 @@ def test_archival_to_version(expected: str, data: dict[str, str]) -> None: def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) config = Configuration() - with pytest.raises(EnvironmentError, match="'hg' was not found"): - parse(str(wd.cwd), config=config) + with pytest.raises(CommandNotFoundError, match=r"hg"): + parse(wd.cwd, config=config) def test_find_files_stop_at_root_hg( From de0827920bfa84ec2af5820bf48f09a0377b323f Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Sep 2023 22:30:52 +0200 Subject: [PATCH 2/3] remove bad deferred locgic from entrypoint selection passing concrete objects instead of the boolean makes it way more safe --- src/setuptools_scm/_entrypoints.py | 15 ++++----------- src/setuptools_scm/_get_version.py | 10 +++++++--- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index 8350968c..e3be053c 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -13,8 +13,8 @@ from . import version if TYPE_CHECKING: - from ._config import Configuration from . import _types as _t + from ._config import Configuration, ParseFunction log = _log.log.getChild("entrypoints") @@ -27,21 +27,14 @@ def load(self) -> Any: pass -def _version_from_entrypoints( - config: Configuration, fallback: bool = False +def version_from_entrypoint( + config: Configuration, entrypoint: str, root: _t.PathT ) -> version.ScmVersion | None: - if fallback: - entrypoint = "setuptools_scm.parse_scm_fallback" - root = config.fallback_root - else: - entrypoint = "setuptools_scm.parse_scm" - root = config.absolute_root - from .discover import iter_matching_entrypoints log.debug("version_from_ep %s in %s", entrypoint, root) for ep in iter_matching_entrypoints(root, entrypoint, config): - fn = ep.load() + fn: ParseFunction = ep.load() maybe_version: version.ScmVersion | None = fn(root, config=config) log.debug("%s found %r", ep, maybe_version) if maybe_version is not None: diff --git a/src/setuptools_scm/_get_version.py b/src/setuptools_scm/_get_version.py index a1afe2d7..59118fbc 100644 --- a/src/setuptools_scm/_get_version.py +++ b/src/setuptools_scm/_get_version.py @@ -8,9 +8,9 @@ from typing import Pattern from . import _config +from . import _entrypoints from . import _types as _t from ._config import Configuration -from ._entrypoints import _version_from_entrypoints from ._overrides import _read_pretended_version_for from ._version_cls import _validate_version_cls from .version import format_version as _format_version @@ -27,11 +27,15 @@ def parse_scm_version(config: Configuration) -> ScmVersion | None: ) return parse_result else: - return _version_from_entrypoints(config) + entrypoint = "setuptools_scm.parse_scm" + root = config.absolute_root + return _entrypoints.version_from_entrypoint(config, entrypoint, root) def parse_fallback_version(config: Configuration) -> ScmVersion | None: - return _version_from_entrypoints(config, fallback=True) + entrypoint = "setuptools_scm.parse_scm_fallback" + root = config.fallback_root + return _entrypoints.version_from_entrypoint(config, entrypoint, root) def _do_parse(config: Configuration) -> ScmVersion | None: From 978564b7a9405500d46d0c4f8a250f751e224536 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Sep 2023 23:40:36 +0200 Subject: [PATCH 3/3] fix #549: when CommandNotFound is triggered, try the fallbacks supersedes #783 --- CHANGELOG.rst | 1 + src/setuptools_scm/_get_version.py | 32 +++++++++++++++++++----------- testing/test_git.py | 4 ++++ testing/test_mercurial.py | 3 +++ 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cb057075..67520e29 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,6 +38,7 @@ features * support passing log levels to SETUPTOOLS_SCM_DEBUG * support using rich.logging as console log handler if installed * fix #527: type annotation in default version template +* fix #549: use fallbacks when scm search raises CommandNotFoundError bugfixes -------- diff --git a/src/setuptools_scm/_get_version.py b/src/setuptools_scm/_get_version.py index 59118fbc..d2fe9e14 100644 --- a/src/setuptools_scm/_get_version.py +++ b/src/setuptools_scm/_get_version.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import re import warnings from pathlib import Path @@ -9,6 +10,7 @@ from . import _config from . import _entrypoints +from . import _run_cmd from . import _types as _t from ._config import Configuration from ._overrides import _read_pretended_version_for @@ -16,20 +18,26 @@ from .version import format_version as _format_version from .version import ScmVersion +_log = logging.getLogger(__name__) + def parse_scm_version(config: Configuration) -> ScmVersion | None: - if config.parse is not None: - parse_result = config.parse(config.absolute_root, config=config) - if parse_result is not None and not isinstance(parse_result, ScmVersion): - raise TypeError( - f"version parse result was {str!r}\n" - "please return a parsed version (ScmVersion)" - ) - return parse_result - else: - entrypoint = "setuptools_scm.parse_scm" - root = config.absolute_root - return _entrypoints.version_from_entrypoint(config, entrypoint, root) + try: + if config.parse is not None: + parse_result = config.parse(config.absolute_root, config=config) + if parse_result is not None and not isinstance(parse_result, ScmVersion): + raise TypeError( + f"version parse result was {str!r}\n" + "please return a parsed version (ScmVersion)" + ) + return parse_result + else: + entrypoint = "setuptools_scm.parse_scm" + root = config.absolute_root + return _entrypoints.version_from_entrypoint(config, entrypoint, root) + except _run_cmd.CommandNotFoundError as e: + _log.exception("command %s not found while parsing the scm, using fallbacks", e) + return None def parse_fallback_version(config: Configuration) -> ScmVersion | None: diff --git a/testing/test_git.py b/testing/test_git.py index 3642ee1a..8bebd1e1 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -94,9 +94,13 @@ def test_root_search_parent_directories( def test_git_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) + + wd.write("pyproject.toml", "[tool.setuptools_scm]") with pytest.raises(CommandNotFoundError, match=r"git"): git.parse(wd.cwd, Configuration(), git.DEFAULT_DESCRIBE) + assert wd.get_version(fallback_version="1.0") == "1.0" + @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/298") @pytest.mark.issue(403) diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index ea50ece5..3aa85948 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -56,9 +56,12 @@ def test_archival_to_version(expected: str, data: dict[str, str]) -> None: def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) config = Configuration() + wd.write("pyproject.toml", "[tool.setuptools_scm]") with pytest.raises(CommandNotFoundError, match=r"hg"): parse(wd.cwd, config=config) + assert wd.get_version(fallback_version="1.0") == "1.0" + def test_find_files_stop_at_root_hg( wd: WorkDir, monkeypatch: pytest.MonkeyPatch