From 0fa8bee903b0f3f8f797a2101771d41698f61968 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 8 Jun 2021 21:43:21 +0200 Subject: [PATCH 01/18] Prepare git archival style testing --- .git_archival.txt | 4 +++ .gitattributes | 1 + MANIFEST.in | 1 + setup.cfg | 1 + src/setuptools_scm/.git_archival.txt | 4 +++ src/setuptools_scm/git.py | 32 +++++++++++++++++++++++ testing/Dockerfile.rawhide-git | 7 +++++ testing/test_git.py | 38 +++++++++++++++++++++++++--- tox.ini | 1 + 9 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 .git_archival.txt create mode 100644 .gitattributes create mode 100644 src/setuptools_scm/.git_archival.txt create mode 100644 testing/Dockerfile.rawhide-git diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 00000000..37d637df --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe)$ +ref-names: $Format:%D$ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..00a7b00c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst diff --git a/MANIFEST.in b/MANIFEST.in index 92e3a537..c758a17a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ exclude *.nix exclude .pre-commit-config.yaml +exclude .git_archival.txt include *.py include testing/*.py include tox.ini diff --git a/setup.cfg b/setup.cfg index 7658a889..fd090cd3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,6 +59,7 @@ setuptools_scm.parse_scm = .git = setuptools_scm.git:parse setuptools_scm.parse_scm_fallback = .hg_archival.txt = setuptools_scm.hg:parse_archival + .git_archival.txt = setuptools_scm.git:parse_archival PKG-INFO = setuptools_scm.hacks:parse_pkginfo pip-egg-info = setuptools_scm.hacks:parse_pip_egg_info setup.py = setuptools_scm.hacks:fallback_version diff --git a/src/setuptools_scm/.git_archival.txt b/src/setuptools_scm/.git_archival.txt new file mode 100644 index 00000000..37d637df --- /dev/null +++ b/src/setuptools_scm/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe)$ +ref-names: $Format:%D$ diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index f6933ff4..1566a12f 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -1,4 +1,5 @@ import os +import re import warnings from datetime import date from datetime import datetime @@ -8,10 +9,15 @@ from .config import Configuration from .scm_workdir import Workdir +from .utils import data_from_mime from .utils import do_ex from .utils import require_command from .utils import trace from .version import meta +from .version import tags_to_versions + +REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b") +DESCRIBE_UNSUPPORTED = "%(describe" # If testing command in shell make sure to quote the match argument like # '*[0-9]*' as it will expand before being sent to git if there are any matching @@ -231,3 +237,29 @@ def search_parent(dirname): if not tail: return None + + +def archival_to_version(data, config=None): + trace("data", data) + archival_describe = data.get("describe-name", DESCRIBE_UNSUPPORTED) + if DESCRIBE_UNSUPPORTED in archival_describe: + warnings.warn("git archive did not support describe output") + else: + tag, number, node, _ = _git_parse_describe(archival_describe) + return meta( + tag, + config=config, + distance=None if number == 0 else number, + node=node, + ) + versions = tags_to_versions(REF_TAG_RE.findall(data.get("ref-names", ""))) + if versions: + return meta(versions[0], config=config) + else: + return meta("0.0", node=data.get("node"), config=config) + + +def parse_archival(root, config=None): + archival = os.path.join(root, ".git_archival.txt") + data = data_from_mime(archival) + return archival_to_version(data, config=config) diff --git a/testing/Dockerfile.rawhide-git b/testing/Dockerfile.rawhide-git new file mode 100644 index 00000000..b92f8c3e --- /dev/null +++ b/testing/Dockerfile.rawhide-git @@ -0,0 +1,7 @@ +FROM fedora:rawhide +RUN dnf install git -y +RUN git --version +USER 1000:1000 +VOLUME /repo +WORKDIR /repo +ENTRYPOINT mkdir git-archived && git archive HEAD -o git-archived/archival.tar.gz diff --git a/testing/test_git.py b/testing/test_git.py index ffb3d3c6..f4990ed4 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -9,10 +9,13 @@ import pytest +from setuptools_scm import Configuration +from setuptools_scm import format_version from setuptools_scm import git from setuptools_scm import integration from setuptools_scm import NonNormalizedVersion from setuptools_scm.file_finder_git import git_find_files +from setuptools_scm.git import archival_to_version from setuptools_scm.utils import do from setuptools_scm.utils import has_command @@ -395,9 +398,7 @@ def parse_date(): assert meta.node_date == today -def test_git_getdate_badgit( - wd, -): +def test_git_getdate_badgit(wd): wd.commit_testfile() git_wd = git.GitWorkdir(os.fspath(wd.cwd)) with patch.object(git_wd, "do_ex", Mock(return_value=("%cI", "", 0))): @@ -435,3 +436,34 @@ def test_git_getdate_signed_commit(signed_commit_wd): signed_commit_wd.commit_testfile(signed=True) git_wd = git.GitWorkdir(os.fspath(signed_commit_wd.cwd)) assert git_wd.get_head_date() == today + + +@pytest.mark.parametrize( + "expected, from_data", + [ + ( + "1.0", + {"describe-name": "1.0-0-g0000"}, + ), + ( + "1.1.dev3+g0000", + { + "describe-name": "1.0-3-g0000", + "node": "0" * 20, + }, + ), + ("0.0", {"node": "0" * 20}), + ("1.2.2", {"describe-name": "release-1.2.2-0-g00000"}), + ("1.2.2.dev0", {"ref-names": "tag: release-1.2.2.dev"}), + ], +) +@pytest.mark.filterwarnings("ignore:git archive did not support describe output") +def test_git_archival_to_version(expected, from_data): + config = Configuration() + version = archival_to_version(from_data, config=config) + assert ( + format_version( + version, version_scheme="guess-next-dev", local_scheme="node-and-date" + ) + == expected + ) diff --git a/tox.ini b/tox.ini index e356153c..65edad1f 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ testpaths=testing filterwarnings= error ignore:.*tool\.setuptools_scm.* + ignore:.*git archive did not support describe output.*:UserWarning markers= issue(id): reference to github issue skip_commit: allows to skip committing in the helpers From 76db4637e707d4bae2306932d6a4e4f3264f3587 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 15 Apr 2022 13:36:16 +0200 Subject: [PATCH 02/18] typing fixed in face of ongoing python 3.6 support --- .pre-commit-config.yaml | 2 + src/setuptools_scm/__init__.py | 2 +- src/setuptools_scm/config.py | 4 +- src/setuptools_scm/utils.py | 23 +++++---- testing/__init__.py | 0 testing/conftest.py | 86 +++++----------------------------- testing/test_git.py | 10 ++-- testing/wd_wrapper.py | 70 +++++++++++++++++++++++++++ 8 files changed, 106 insertions(+), 91 deletions(-) create mode 100644 testing/__init__.py create mode 100644 testing/wd_wrapper.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd142d91..a95605d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,5 @@ +default_language_version: + python: python3.9 repos: - repo: https://github.com/psf/black rev: 22.3.0 diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py index 10134b67..aa4931ae 100644 --- a/src/setuptools_scm/__init__.py +++ b/src/setuptools_scm/__init__.py @@ -85,7 +85,7 @@ def _do_parse(config: Configuration) -> "ScmVersion|None": parse_result = _call_entrypoint_fn(config.absolute_root, config, config.parse) if isinstance(parse_result, str): raise TypeError( - "version parse result was a string\nplease return a parsed version" + f"version parse result was {str!r}\nplease return a parsed version" ) version: Optional[ScmVersion] if parse_result: diff --git a/src/setuptools_scm/config.py b/src/setuptools_scm/config.py index 4ad32ac8..850635ad 100644 --- a/src/setuptools_scm/config.py +++ b/src/setuptools_scm/config.py @@ -30,7 +30,7 @@ def _check_tag_regex(value): return regex -def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT): +def _check_absolute_root(root: _t.PathT, relative_to: "_t.PathT | None"): trace("abs root", repr(locals())) if relative_to: if ( @@ -85,7 +85,7 @@ def __init__( fallback_root: _t.PathT = ".", parse=None, git_describe_command=None, - dist_name: str = None, + dist_name: "str|None" = None, version_cls: "Type[Version]|Type[NonNormalizedVersion]|str|None" = None, normalize: bool = True, search_parent_directories: bool = False, diff --git a/src/setuptools_scm/utils.py b/src/setuptools_scm/utils.py index 3c80fe0c..744cade8 100644 --- a/src/setuptools_scm/utils.py +++ b/src/setuptools_scm/utils.py @@ -8,9 +8,12 @@ import subprocess import sys import warnings +from typing import cast +from typing import Dict from typing import List -from typing import Optional +from typing import Tuple +from . import _types as _t DEBUG = bool(os.environ.get("SETUPTOOLS_SCM_DEBUG")) IS_WINDOWS = platform.system() == "Windows" @@ -42,24 +45,24 @@ def trace(*k) -> None: print(*k, file=sys.stderr, flush=True) -def ensure_stripped_str(str_or_bytes): +def ensure_stripped_str(str_or_bytes: "str | bytes") -> str: if isinstance(str_or_bytes, str): return str_or_bytes.strip() else: return str_or_bytes.decode("utf-8", "surrogateescape").strip() -def _always_strings(env_dict): +def _always_strings(env_dict: dict) -> Dict[str, str]: """ On Windows and Python 2, environment dictionaries must be strings and not unicode. """ if IS_WINDOWS: env_dict.update((key, str(value)) for (key, value) in env_dict.items()) - return env_dict + return cast(Dict[str, str], env_dict) -def _popen_pipes(cmd, cwd): +def _popen_pipes(cmd, cwd: "str | _t.PathT"): return subprocess.Popen( cmd, stdout=subprocess.PIPE, @@ -78,7 +81,7 @@ def _popen_pipes(cmd, cwd): ) -def do_ex(cmd, cwd="."): +def do_ex(cmd, cwd: "str | _t.PathT" = ".") -> Tuple[str, str, int]: trace("cmd", repr(cmd)) trace(" in", cwd) if os.name == "posix" and not isinstance(cmd, (list, tuple)): @@ -95,14 +98,14 @@ def do_ex(cmd, cwd="."): return ensure_stripped_str(out), ensure_stripped_str(err), p.returncode -def do(cmd, cwd="."): +def do(cmd: "List[str] | str", cwd: "str | _t.PathT" = "."): out, err, ret = do_ex(cmd, cwd) if ret: print(err) return out -def data_from_mime(path): +def data_from_mime(path: _t.PathT) -> Dict[str, str]: with open(path, encoding="utf-8") as fp: content = fp.read() trace("content", repr(content)) @@ -112,7 +115,7 @@ def data_from_mime(path): return data -def function_has_arg(fn, argname): +def function_has_arg(fn: object, argname: str) -> bool: assert inspect.isfunction(fn) argspec = inspect.signature(fn).parameters @@ -120,7 +123,7 @@ def function_has_arg(fn, argname): return argname in argspec -def has_command(name: str, args: Optional[List[str]] = None, warn: bool = True) -> bool: +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 = _popen_pipes(cmd, ".") diff --git a/testing/__init__.py b/testing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testing/conftest.py b/testing/conftest.py index 6d9a84a3..7691215e 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,8 +1,10 @@ -import itertools import os import pytest +from .wd_wrapper import WorkDir + + # 2009-02-13T23:31:30+00:00 os.environ["SOURCE_DATE_EPOCH"] = "1234567890" os.environ["SETUPTOOLS_SCM_DEBUG"] = "1" @@ -10,13 +12,15 @@ def pytest_report_header(): - import pkg_resources - + try: + from importlib.metadata import version + except ImportError: + from importlib_metadata import version res = [] for pkg in VERSION_PKGS: - version = pkg_resources.get_distribution(pkg).version + pkg_version = version(pkg) path = __import__(pkg).__file__ - res.append(f"{pkg} version {version} from {path!r}") + res.append(f"{pkg} version {pkg_version} from {path!r}") return res @@ -27,72 +31,6 @@ def pytest_addoption(parser): ) -class Wd: - commit_command = None - signed_commit_command = None - add_command = None - - def __repr__(self): - return f"" - - def __init__(self, cwd): - self.cwd = cwd - self.__counter = itertools.count() - - def __call__(self, cmd, **kw): - if kw: - cmd = cmd.format(**kw) - from setuptools_scm.utils import do - - return do(cmd, self.cwd) - - def write(self, name, value, **kw): - filename = self.cwd / name - if kw: - value = value.format(**kw) - if isinstance(value, bytes): - filename.write_bytes(value) - else: - filename.write_text(value) - return filename - - def _reason(self, given_reason): - if given_reason is None: - return f"number-{next(self.__counter)}" - else: - return given_reason - - def add_and_commit(self, reason=None, **kwargs): - self(self.add_command) - self.commit(reason, **kwargs) - - def commit(self, reason=None, signed=False): - reason = self._reason(reason) - self( - self.commit_command if not signed else self.signed_commit_command, - reason=reason, - ) - - def commit_testfile(self, reason=None, **kwargs): - reason = self._reason(reason) - self.write("test.txt", "test {reason}", reason=reason) - self(self.add_command) - self.commit(reason=reason, **kwargs) - - def get_version(self, **kw): - __tracebackhide__ = True - from setuptools_scm import get_version - - version = get_version(root=str(self.cwd), fallback_root=str(self.cwd), **kw) - print(version) - return version - - @property - def version(self): - __tracebackhide__ = True - return self.get_version() - - @pytest.fixture(autouse=True) def debug_mode(): from setuptools_scm import utils @@ -106,7 +44,7 @@ def debug_mode(): def wd(tmp_path): target_wd = tmp_path.resolve() / "wd" target_wd.mkdir() - return Wd(target_wd) + return WorkDir(target_wd) @pytest.fixture @@ -117,7 +55,7 @@ def repositories_hg_git(tmp_path): path_git = tmp_path / "repo_git" path_git.mkdir() - wd = Wd(path_git) + wd = WorkDir(path_git) wd("git init") wd("git config user.email test@example.com") wd('git config user.name "a test"') @@ -131,7 +69,7 @@ def repositories_hg_git(tmp_path): with open(path_hg / ".hg/hgrc", "a") as file: file.write("[extensions]\nhggit =\n") - wd_hg = Wd(path_hg) + wd_hg = WorkDir(path_hg) wd_hg.add_command = "hg add ." wd_hg.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' diff --git a/testing/test_git.py b/testing/test_git.py index f4990ed4..f1aa6a43 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -98,7 +98,8 @@ def test_parse_call_order(wd): def test_version_from_git(wd): assert wd.version == "0.1.dev0" - assert git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE).branch == "master" + parsed = git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE) + assert parsed is not None and parsed.branch == "master" wd.commit_testfile() assert wd.version.startswith("0.1.dev1+g") @@ -386,7 +387,9 @@ def test_git_getdate(wd): today = date.today() def parse_date(): - return git.parse(os.fspath(wd.cwd)).node_date + parsed = git.parse(os.fspath(wd.cwd)) + assert parsed is not None + return parsed.node_date git_wd = git.GitWorkdir(os.fspath(wd.cwd)) assert git_wd.get_head_date() is None @@ -394,8 +397,7 @@ def parse_date(): wd.commit_testfile() assert git_wd.get_head_date() == today - meta = git.parse(os.fspath(wd.cwd)) - assert meta.node_date == today + assert parse_date() == today def test_git_getdate_badgit(wd): diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py new file mode 100644 index 00000000..5af2c78c --- /dev/null +++ b/testing/wd_wrapper.py @@ -0,0 +1,70 @@ +import itertools +from pathlib import Path + + +class WorkDir: + """a simple model for a""" + + commit_command: str + signed_commit_command: str + add_command: str + + def __repr__(self): + return f"" + + def __init__(self, cwd: Path): + self.cwd = cwd + self.__counter = itertools.count() + + def __call__(self, cmd, **kw): + if kw: + cmd = cmd.format(**kw) + from setuptools_scm.utils import do + + return do(cmd, self.cwd) + + def write(self, name, value, **kw): + filename = self.cwd / name + if kw: + value = value.format(**kw) + if isinstance(value, bytes): + filename.write_bytes(value) + else: + filename.write_text(value) + return filename + + def _reason(self, given_reason: "str | None") -> str: + if given_reason is None: + return f"number-{next(self.__counter)}" + else: + return given_reason + + def add_and_commit(self, reason=None, **kwargs): + self(self.add_command) + self.commit(reason, **kwargs) + + def commit(self, reason=None, signed=False): + reason = self._reason(reason) + self( + self.commit_command if not signed else self.signed_commit_command, + reason=reason, + ) + + def commit_testfile(self, reason=None, **kwargs): + reason = self._reason(reason) + self.write("test.txt", "test {reason}", reason=reason) + self(self.add_command) + self.commit(reason=reason, **kwargs) + + def get_version(self, **kw): + __tracebackhide__ = True + from setuptools_scm import get_version + + version = get_version(root=str(self.cwd), fallback_root=str(self.cwd), **kw) + print(version) + return version + + @property + def version(self): + __tracebackhide__ = True + return self.get_version() From 982af17c317d1181d3dde6c30adbfaf80120d7c2 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 15 Apr 2022 14:57:39 +0200 Subject: [PATCH 03/18] harden test_version_from git and more type fixes --- testing/test_git.py | 85 ++++++++++++++++++++++--------------------- testing/wd_wrapper.py | 17 +++++---- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/testing/test_git.py b/testing/test_git.py index f1aa6a43..44fbd7fc 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -4,11 +4,14 @@ from datetime import datetime from datetime import timezone from os.path import join as opj +from textwrap import dedent +from typing import Dict from unittest.mock import Mock from unittest.mock import patch import pytest +from .wd_wrapper import WorkDir from setuptools_scm import Configuration from setuptools_scm import format_version from setuptools_scm import git @@ -19,7 +22,6 @@ from setuptools_scm.utils import do from setuptools_scm.utils import has_command - pytestmark = pytest.mark.skipif( not has_command("git", warn=False), reason="git executable not found" ) @@ -58,11 +60,11 @@ def test_root_relative_to(tmpdir, wd, monkeypatch): "relative_to": __file__}) """ ) - res = do((sys.executable, "setup.py", "--version"), p) + res = do([sys.executable, "setup.py", "--version"], p) assert res == "0.1.dev0" -def test_root_search_parent_directories(tmpdir, wd, monkeypatch): +def test_root_search_parent_directories(tmpdir, wd: WorkDir, monkeypatch): monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") p = wd.cwd.joinpath("sub/package") p.mkdir(parents=True) @@ -71,7 +73,7 @@ def test_root_search_parent_directories(tmpdir, wd, monkeypatch): setup(use_scm_version={"search_parent_directories": True}) """ ) - res = do((sys.executable, "setup.py", "--version"), p) + res = do([sys.executable, "setup.py", "--version"], p) assert res == "0.1.dev0" @@ -137,60 +139,59 @@ def test_version_from_git(wd): ) -@pytest.mark.parametrize("with_class", [False, type, str]) -def test_git_version_unnormalized_setuptools(with_class, tmpdir, wd, monkeypatch): +setup_py_with_normalize: Dict[str, str] = { + "false": """ + from setuptools import setup + setup(use_scm_version={'normalize': False, 'write_to': 'VERSION.txt'}) + """, + "with_created_class": """ + from setuptools import setup + + class MyVersion: + def __init__(self, tag_str: str): + self.version = tag_str + + def __repr__(self): + return self.version + + setup(use_scm_version={'version_cls': MyVersion, 'write_to': 'VERSION.txt'}) + """, + "with_named_import": """ + from setuptools import setup + setup(use_scm_version={ + 'version_cls': 'setuptools_scm.NonNormalizedVersion', + 'write_to': 'VERSION.txt' + }) + """, +} + + +@pytest.mark.parametrize( + "setup_py_txt", + [pytest.param(text, id=key) for key, text in setup_py_with_normalize.items()], +) +def test_git_version_unnormalized_setuptools( + setup_py_txt: str, wd: WorkDir, monkeypatch +): """ Test that when integrating with setuptools without normalization, the version is not normalized in write_to files, but still normalized by setuptools for the final dist metadata. """ monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") - p = wd.cwd - - # create a setup.py - dest_file = str(tmpdir.join("VERSION.txt")).replace("\\", "/") - if with_class is False: - # try normalize = False - setup_py = """ -from setuptools import setup -setup(use_scm_version={'normalize': False, 'write_to': '%s'}) -""" - elif with_class is type: - # custom non-normalizing class - setup_py = """ -from setuptools import setup - -class MyVersion: - def __init__(self, tag_str: str): - self.version = tag_str - - def __repr__(self): - return self.version - -setup(use_scm_version={'version_cls': MyVersion, 'write_to': '%s'}) -""" - elif with_class is str: - # non-normalizing class referenced by name - setup_py = """from setuptools import setup -setup(use_scm_version={ - 'version_cls': 'setuptools_scm.NonNormalizedVersion', - 'write_to': '%s' -}) -""" - # finally write the setup.py file - p.joinpath("setup.py").write_text(setup_py % dest_file) + wd.write("setup.py", dedent(setup_py_txt)) # do git operations and tag wd.commit_testfile() wd("git tag 17.33.0-rc1") # setuptools still normalizes using packaging.Version (removing the dash) - res = do((sys.executable, "setup.py", "--version"), p) + res = wd([sys.executable, "setup.py", "--version"]) assert res == "17.33.0rc1" # but the version tag in the file is non-normalized (with the dash) - assert tmpdir.join("VERSION.txt").read() == "17.33.0-rc1" + assert wd.cwd.joinpath("VERSION.txt").read_text() == "17.33.0-rc1" @pytest.mark.issue(179) diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py index 5af2c78c..06993510 100644 --- a/testing/wd_wrapper.py +++ b/testing/wd_wrapper.py @@ -1,5 +1,6 @@ import itertools from pathlib import Path +from typing import List class WorkDir: @@ -12,20 +13,22 @@ class WorkDir: def __repr__(self): return f"" - def __init__(self, cwd: Path): + def __init__(self, cwd: Path) -> None: self.cwd = cwd self.__counter = itertools.count() - def __call__(self, cmd, **kw): + def __call__(self, cmd: "List[str] | str", **kw): if kw: + assert isinstance(cmd, str), "formatting the command requires text input" cmd = cmd.format(**kw) from setuptools_scm.utils import do return do(cmd, self.cwd) - def write(self, name, value, **kw): + def write(self, name: str, value: "str | bytes", **kw: object) -> Path: filename = self.cwd / name if kw: + assert isinstance(value, str) value = value.format(**kw) if isinstance(value, bytes): filename.write_bytes(value) @@ -39,22 +42,22 @@ def _reason(self, given_reason: "str | None") -> str: else: return given_reason - def add_and_commit(self, reason=None, **kwargs): + def add_and_commit(self, reason: "str | None" = None, **kwargs): self(self.add_command) self.commit(reason, **kwargs) - def commit(self, reason=None, signed=False): + def commit(self, reason: "str | None" = None, signed: bool = False) -> None: reason = self._reason(reason) self( self.commit_command if not signed else self.signed_commit_command, reason=reason, ) - def commit_testfile(self, reason=None, **kwargs): + def commit_testfile(self, reason: "str | None" = None, signed: bool = False): reason = self._reason(reason) self.write("test.txt", "test {reason}", reason=reason) self(self.add_command) - self.commit(reason=reason, **kwargs) + self.commit(reason=reason, signed=signed) def get_version(self, **kw): __tracebackhide__ = True From ae83127aa5ab645aaee3bc4f12699f737295f6af Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 15 Apr 2022 17:46:53 +0200 Subject: [PATCH 04/18] refactor test util Workdir.write flor clarity --- testing/wd_wrapper.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py index 06993510..03e24808 100644 --- a/testing/wd_wrapper.py +++ b/testing/wd_wrapper.py @@ -25,16 +25,16 @@ def __call__(self, cmd: "List[str] | str", **kw): return do(cmd, self.cwd) - def write(self, name: str, value: "str | bytes", **kw: object) -> Path: - filename = self.cwd / name + def write(self, name: str, content: "str | bytes", /, **kw: object) -> Path: + path = self.cwd / name if kw: - assert isinstance(value, str) - value = value.format(**kw) - if isinstance(value, bytes): - filename.write_bytes(value) + assert isinstance(content, str) + content = content.format(**kw) + if isinstance(content, bytes): + path.write_bytes(content) else: - filename.write_text(value) - return filename + path.write_text(content) + return path def _reason(self, given_reason: "str | None") -> str: if given_reason is None: From 89d3b51dae0d868560f04df6465966e2c9cbaf72 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 15 Apr 2022 17:56:01 +0200 Subject: [PATCH 05/18] restore python 3.6 support --- testing/wd_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py index 03e24808..e7c92d0f 100644 --- a/testing/wd_wrapper.py +++ b/testing/wd_wrapper.py @@ -25,7 +25,7 @@ def __call__(self, cmd: "List[str] | str", **kw): return do(cmd, self.cwd) - def write(self, name: str, content: "str | bytes", /, **kw: object) -> Path: + def write(self, name: str, content: "str | bytes", **kw: object) -> Path: path = self.cwd / name if kw: assert isinstance(content, str) From e09403f0f8d0923f5fac3f7094e0bbf80c824933 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 15 Apr 2022 22:58:49 +0200 Subject: [PATCH 06/18] use full registry name for fedora image --- testing/Dockerfile.rawhide-git | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/Dockerfile.rawhide-git b/testing/Dockerfile.rawhide-git index b92f8c3e..d9f4ddc8 100644 --- a/testing/Dockerfile.rawhide-git +++ b/testing/Dockerfile.rawhide-git @@ -1,4 +1,4 @@ -FROM fedora:rawhide +FROM registry.fedoraproject.org/fedora:rawhide RUN dnf install git -y RUN git --version USER 1000:1000 From b45e19f9f275a31873fd5e07faabef16fd0bbec0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 7 May 2022 21:12:35 +0200 Subject: [PATCH 07/18] complete mypy transformation --- .pre-commit-config.yaml | 8 +- setup.py | 11 +- src/setuptools_scm/__init__.py | 53 ++++--- src/setuptools_scm/__main__.py | 81 +--------- src/setuptools_scm/_cli.py | 80 ++++++++++ src/setuptools_scm/_entrypoints.py | 57 +++++-- src/setuptools_scm/_types.py | 39 +++-- src/setuptools_scm/_version_cls.py | 42 +---- src/setuptools_scm/config.py | 85 ++++++---- src/setuptools_scm/discover.py | 15 +- src/setuptools_scm/file_finder.py | 19 ++- src/setuptools_scm/file_finder_git.py | 20 ++- src/setuptools_scm/file_finder_hg.py | 13 +- src/setuptools_scm/git.py | 87 ++++++---- src/setuptools_scm/hacks.py | 27 +++- src/setuptools_scm/hg.py | 103 +++++++----- src/setuptools_scm/hg_git.py | 84 ++++++---- src/setuptools_scm/integration.py | 25 ++- src/setuptools_scm/scm_workdir.py | 11 +- src/setuptools_scm/utils.py | 54 +++---- src/setuptools_scm/version.py | 220 +++++++++++++++++--------- testing/conftest.py | 19 ++- testing/test_basic_api.py | 96 ++++++----- testing/test_config.py | 9 +- testing/test_file_finder.py | 51 +++--- testing/test_functions.py | 25 +-- testing/test_git.py | 96 ++++++----- testing/test_hg_git.py | 7 +- testing/test_integration.py | 47 +++--- testing/test_main.py | 21 +-- testing/test_mercurial.py | 42 +++-- testing/test_regressions.py | 67 ++++---- testing/test_setuptools_support.py | 43 +++-- testing/test_version.py | 71 ++++++--- testing/wd_wrapper.py | 19 ++- 35 files changed, 1048 insertions(+), 699 deletions(-) create mode 100644 src/setuptools_scm/_cli.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a95605d8..ba8933a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: black args: [--safe, --quiet] - repo: https://github.com/asottile/reorder_python_imports - rev: v3.0.1 + rev: v3.1.0 hooks: - id: reorder-python-imports args: [ "--application-directories=.:src" , --py3-plus] @@ -32,10 +32,12 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.942' + rev: 'v0.950' hooks: - id: mypy + args: [--strict] + language_version: "3.10" additional_dependencies: - types-setuptools - tokenize-rt==3.2.0 - - pytest == 6.2.5 + - pytest == 7.1 diff --git a/setup.py b/setup.py index 4bed0bff..c1627285 100644 --- a/setup.py +++ b/setup.py @@ -11,20 +11,22 @@ """ import os import sys +from typing import NoReturn +from typing import Optional import setuptools from setuptools.command.bdist_egg import bdist_egg as original_bdist_egg class bdist_egg(original_bdist_egg): - def run(self): + def run(self) -> NoReturn: raise SystemExit( "%s is forbidden, " "please update to setuptools>=45 which uses pip" % type(self).__name__ ) -def scm_version(): +def scm_version() -> str: if sys.version_info < (3, 6): raise RuntimeError( @@ -40,8 +42,11 @@ def scm_version(): from setuptools_scm import git from setuptools_scm import hg from setuptools_scm.version import guess_next_dev_version, get_local_node_and_date + from setuptools_scm.config import Configuration - def parse(root, config): + from setuptools_scm.version import ScmVersion + + def parse(root: str, config: Configuration) -> Optional[ScmVersion]: try: return parse_pkginfo(root, config) except OSError: diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py index aa4931ae..9e53371f 100644 --- a/src/setuptools_scm/__init__.py +++ b/src/setuptools_scm/__init__.py @@ -4,10 +4,13 @@ """ import os import warnings +from typing import Any +from typing import Callable from typing import Optional from typing import TYPE_CHECKING +from typing import Union -from . import _types +from . import _types as _t from ._entrypoints import _call_entrypoint_fn from ._entrypoints import _version_from_entrypoints from ._overrides import _read_pretended_version_for @@ -36,14 +39,14 @@ # coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control -version = {version!r} -version_tuple = {version_tuple!r} +__version__ = version = {version!r} +__version_tuple__ = version_tuple = {version_tuple!r} """, ".txt": "{version}", } -def version_from_scm(root): +def version_from_scm(root: _t.PathT) -> Optional[ScmVersion]: warnings.warn( "version_from_scm is deprecated please use get_version", category=DeprecationWarning, @@ -54,11 +57,11 @@ def version_from_scm(root): def dump_version( - root: _types.PathT, + root: _t.PathT, version: str, - write_to: _types.PathT, + write_to: _t.PathT, template: "str | None" = None, -): +) -> None: assert isinstance(version, str) target = os.path.normpath(os.path.join(root, write_to)) ext = os.path.splitext(target)[1] @@ -102,7 +105,7 @@ def _do_parse(config: Configuration) -> "ScmVersion|None": return version -def _version_missing(config) -> "NoReturn": +def _version_missing(config: Configuration) -> "NoReturn": raise LookupError( f"setuptools-scm was unable to detect version for {config.absolute_root}.\n\n" "Make sure you're either building from a fully intact git repository " @@ -116,23 +119,23 @@ def _version_missing(config) -> "NoReturn": def get_version( - root=".", - version_scheme=DEFAULT_VERSION_SCHEME, - local_scheme=DEFAULT_LOCAL_SCHEME, - write_to=None, - write_to_template=None, - relative_to=None, - tag_regex=DEFAULT_TAG_REGEX, - parentdir_prefix_version=None, - fallback_version=None, - fallback_root=".", - parse=None, - git_describe_command=None, - dist_name=None, - version_cls=None, - normalize=True, - search_parent_directories=False, -): + root: str = ".", + version_scheme: Union[Callable[[ScmVersion], str], str] = DEFAULT_VERSION_SCHEME, + local_scheme: Union[Callable[[ScmVersion], str], str] = DEFAULT_LOCAL_SCHEME, + write_to: Optional[_t.PathT] = None, + write_to_template: Optional[str] = None, + relative_to: Optional[str] = None, + tag_regex: str = DEFAULT_TAG_REGEX, + parentdir_prefix_version: Optional[str] = None, + fallback_version: Optional[str] = None, + fallback_root: _t.PathT = ".", + parse: Optional[Any] = None, + git_describe_command: Optional[Any] = None, + dist_name: Optional[str] = None, + version_cls: Optional[Any] = None, + normalize: bool = True, + search_parent_directories: bool = False, +) -> str: """ If supplied, relative_to should be a file from which root may be resolved. Typically called by a script or module that is not diff --git a/src/setuptools_scm/__main__.py b/src/setuptools_scm/__main__.py index c00085f3..1b616792 100644 --- a/src/setuptools_scm/__main__.py +++ b/src/setuptools_scm/__main__.py @@ -1,83 +1,4 @@ -import argparse -import os -import sys - -from setuptools_scm import _get_version -from setuptools_scm.config import Configuration -from setuptools_scm.discover import walk_potential_roots -from setuptools_scm.integration import find_files - - -def main() -> None: - opts = _get_cli_opts() - root = opts.root or "." - - try: - pyproject = opts.config or _find_pyproject(root) - root = opts.root or os.path.relpath(os.path.dirname(pyproject)) - config = Configuration.from_file(pyproject, root=root) - except (LookupError, FileNotFoundError) as ex: - # no pyproject.toml OR no [tool.setuptools_scm] - print( - f"Warning: could not use {os.path.relpath(pyproject)}," - " using default configuration.\n" - f" Reason: {ex}.", - file=sys.stderr, - ) - config = Configuration(root=root) - - version = _get_version(config) - assert version is not None - if opts.strip_dev: - version = version.partition(".dev")[0] - print(version) - - if opts.command == "ls": - for fname in find_files(config.root): - print(fname) - - -def _get_cli_opts() -> argparse.Namespace: - prog = "python -m setuptools_scm" - desc = "Print project version according to SCM metadata" - parser = argparse.ArgumentParser(prog, description=desc) - # By default, help for `--help` starts with lower case, so we keep the pattern: - parser.add_argument( - "-r", - "--root", - default=None, - help='directory managed by the SCM, default: inferred from config file, or "."', - ) - parser.add_argument( - "-c", - "--config", - default=None, - metavar="PATH", - help="path to 'pyproject.toml' with setuptools_scm config, " - "default: looked up in the current or parent directories", - ) - parser.add_argument( - "--strip-dev", - action="store_true", - help="remove the dev/local parts of the version before printing the version", - ) - sub = parser.add_subparsers(title="extra commands", dest="command", metavar="") - # We avoid `metavar` to prevent printing repetitive information - desc = "List files managed by the SCM" - sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc) - return parser.parse_args() - - -def _find_pyproject(parent: str) -> str: - for directory in walk_potential_roots(os.path.abspath(parent)): - pyproject = os.path.join(directory, "pyproject.toml") - if os.path.isfile(pyproject): - return pyproject - - return os.path.abspath( - "pyproject.toml" - ) # use default name to trigger the default errors - +from ._cli import main if __name__ == "__main__": main() diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py new file mode 100644 index 00000000..0e0ad77b --- /dev/null +++ b/src/setuptools_scm/_cli.py @@ -0,0 +1,80 @@ +import argparse +import os +import sys + +from setuptools_scm import _get_version +from setuptools_scm.config import Configuration +from setuptools_scm.discover import walk_potential_roots +from setuptools_scm.integration import find_files + + +def main() -> None: + opts = _get_cli_opts() + root = opts.root or "." + + pyproject = opts.config or _find_pyproject(root) + + try: + root = opts.root or os.path.relpath(os.path.dirname(pyproject)) + config = Configuration.from_file(pyproject, root=root) + except (LookupError, FileNotFoundError) as ex: + # no pyproject.toml OR no [tool.setuptools_scm] + print( + f"Warning: could not use {os.path.relpath(pyproject)}," + " using default configuration.\n" + f" Reason: {ex}.", + file=sys.stderr, + ) + config = Configuration(root=root) + + version = _get_version(config) + assert version is not None + if opts.strip_dev: + version = version.partition(".dev")[0] + print(version) + + if opts.command == "ls": + for fname in find_files(config.root): + print(fname) + + +def _get_cli_opts() -> argparse.Namespace: + prog = "python -m setuptools_scm" + desc = "Print project version according to SCM metadata" + parser = argparse.ArgumentParser(prog, description=desc) + # By default, help for `--help` starts with lower case, so we keep the pattern: + parser.add_argument( + "-r", + "--root", + default=None, + help='directory managed by the SCM, default: inferred from config file, or "."', + ) + parser.add_argument( + "-c", + "--config", + default=None, + metavar="PATH", + help="path to 'pyproject.toml' with setuptools_scm config, " + "default: looked up in the current or parent directories", + ) + parser.add_argument( + "--strip-dev", + action="store_true", + help="remove the dev/local parts of the version before printing the version", + ) + sub = parser.add_subparsers(title="extra commands", dest="command", metavar="") + # We avoid `metavar` to prevent printing repetitive information + desc = "List files managed by the SCM" + sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc) + return parser.parse_args() + + +def _find_pyproject(parent: str) -> str: + for directory in walk_potential_roots(os.path.abspath(parent)): + pyproject = os.path.join(directory, "pyproject.toml") + if os.path.isfile(pyproject): + return pyproject + + return os.path.abspath( + "pyproject.toml" + ) # use default name to trigger the default errors diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index b1cefd3e..299ddaa4 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -1,14 +1,37 @@ import warnings +from typing import Any +from typing import Iterator from typing import Optional +from typing import overload +from typing import Protocol +from typing import TYPE_CHECKING -from .config import Configuration -from .discover import iter_matching_entrypoints +from . import _types as _t from .utils import function_has_arg from .utils import trace -from setuptools_scm.version import ScmVersion +from .version import ScmVersion +if TYPE_CHECKING: + from .config import Configuration +else: + Configuration = Any + + +class MaybeConfigFunction(Protocol): + __name__: str + + @overload + def __call__(self, root: _t.PathT, config: Configuration) -> Optional[ScmVersion]: + pass + + @overload + def __call__(self, root: _t.PathT) -> Optional[ScmVersion]: + pass -def _call_entrypoint_fn(root, config, fn): + +def _call_entrypoint_fn( + root: _t.PathT, config: Configuration, fn: MaybeConfigFunction +) -> Optional[ScmVersion]: if function_has_arg(fn, "config"): return fn(root, config=config) else: @@ -32,6 +55,8 @@ def _version_from_entrypoints( entrypoint = "setuptools_scm.parse_scm" root = config.absolute_root + from .discover import iter_matching_entrypoints + trace("version_from_ep", entrypoint, root) for ep in iter_matching_entrypoints(root, entrypoint, config): version: Optional[ScmVersion] = _call_entrypoint_fn(root, config, ep.load()) @@ -44,15 +69,17 @@ def _version_from_entrypoints( try: from importlib.metadata import entry_points # type: ignore except ImportError: - from pkg_resources import iter_entry_points -else: + from importlib_metadata import entry_points # type: ignore - def iter_entry_points(group: str, name: Optional[str] = None): - 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) + +def iter_entry_points( + group: str, name: Optional[str] = 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) diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index 923931c2..09fb2b65 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -1,31 +1,52 @@ import os +import sys +from typing import Any from typing import Callable +from typing import List +from typing import NamedTuple +from typing import Protocol from typing import TYPE_CHECKING from typing import TypeVar from typing import Union + if TYPE_CHECKING: + from setuptools_scm import version - from typing_extensions import ParamSpec +if sys.version_info >= (3, 9): + from typing import ParamSpec, TypeAlias else: + from typing_extensions import ParamSpec, TypeAlias + +PathT = Union["os.PathLike[str]", str] - class ParamSpec(list): - def __init__(self, _) -> None: - pass +CMD_TYPE: TypeAlias = Union[List[str], str] +VERSION_SCHEME = Union[str, Callable[["version.ScmVersion"], str]] -PathT = Union["os.PathLike[str]", str] + +class CmdResult(NamedTuple): + out: str + err: str + returncode: int + + +class EntrypointProtocol(Protocol): + name: str + + def load(self) -> Any: + pass T = TypeVar("T") T2 = TypeVar("T2") -PARAMS = ParamSpec("PARAMS") +P = ParamSpec("P") def transfer_input_args( - template: "Callable[PARAMS, T]", -) -> Callable[[Callable[..., T2]], "Callable[PARAMS, T2]"]: - def decorate(func: Callable[..., T2]) -> "Callable[PARAMS, T2]": + 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 240a97d5..7bde4b28 100644 --- a/src/setuptools_scm/_version_cls.py +++ b/src/setuptools_scm/_version_cls.py @@ -1,35 +1,9 @@ from logging import getLogger from typing import Tuple +from typing import Union -try: - from packaging.version import Version, InvalidVersion - - assert hasattr( - Version, "release" - ), "broken installation ensure packaging>=20 is available" -except ImportError: - from pkg_resources._vendor.packaging.version import ( # type: ignore - Version as SetuptoolsVersion, - InvalidVersion, - ) - - try: - SetuptoolsVersion.release - Version = SetuptoolsVersion # type: ignore - except AttributeError: - - class Version(SetuptoolsVersion): # type: ignore - @property - def release(self): - return self._version.release - - @property - def dev(self): - return self._version.dev - - @property - def local(self): - return self._version.local +from packaging.version import InvalidVersion +from packaging.version import Version as Version class NonNormalizedVersion(Version): @@ -41,23 +15,23 @@ class NonNormalizedVersion(Version): trust the version tags. """ - def __init__(self, version): + def __init__(self, version: str) -> None: # parse and validate using parent super().__init__(version) # store raw for str self._raw_version = version - def __str__(self): + def __str__(self) -> str: # return the non-normalized version (parent returns the normalized) return self._raw_version - def __repr__(self): + def __repr__(self) -> str: # same pattern as parent return f"" -def _version_as_tuple(version_str) -> Tuple["int | str", ...]: +def _version_as_tuple(version_str: str) -> Tuple[Union[int, str], ...]: try: parsed_version = Version(version_str) except InvalidVersion: @@ -66,7 +40,7 @@ def _version_as_tuple(version_str) -> Tuple["int | str", ...]: log.exception("failed to parse version %s", version_str) return (version_str,) else: - version_fields: Tuple["int | str", ...] = parsed_version.release + version_fields: Tuple[Union[int, str], ...] = parsed_version.release if parsed_version.dev is not None: version_fields += (f"dev{parsed_version.dev}",) if parsed_version.local is not None: diff --git a/src/setuptools_scm/config.py b/src/setuptools_scm/config.py index 850635ad..eba15e92 100644 --- a/src/setuptools_scm/config.py +++ b/src/setuptools_scm/config.py @@ -2,20 +2,30 @@ import os import re import warnings +from typing import Any +from typing import Callable +from typing import Dict +from typing import Optional +from typing import Pattern from typing import Type -from typing import TypeVar +from typing import TYPE_CHECKING +from typing import Union from . import _types as _t from ._version_cls import NonNormalizedVersion from ._version_cls import Version from .utils import trace + +if TYPE_CHECKING: + from setuptools_scm.version import ScmVersion + DEFAULT_TAG_REGEX = r"^(?:[\w-]+-)?(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$" DEFAULT_VERSION_SCHEME = "guess-next-dev" DEFAULT_LOCAL_SCHEME = "node-and-date" -def _check_tag_regex(value): +def _check_tag_regex(value: Optional[Union[str, Pattern[str]]]) -> Pattern[str]: if not value: value = DEFAULT_TAG_REGEX regex = re.compile(value) @@ -30,7 +40,7 @@ def _check_tag_regex(value): return regex -def _check_absolute_root(root: _t.PathT, relative_to: "_t.PathT | None"): +def _check_absolute_root(root: _t.PathT, relative_to: Optional[_t.PathT]) -> str: trace("abs root", repr(locals())) if relative_to: if ( @@ -55,55 +65,60 @@ def _check_absolute_root(root: _t.PathT, relative_to: "_t.PathT | None"): return os.path.abspath(root) -def _lazy_tomli_load(data: str): +def _lazy_tomli_load(data: str) -> Dict[str, Any]: from tomli import loads return loads(data) -VersionT = TypeVar("VersionT", Version, NonNormalizedVersion) +VersionT = Union[Version, NonNormalizedVersion] class Configuration: """Global configuration model""" - _root: _t.PathT - _relative_to: "_t.PathT | None" - version_cls: "Type[Version]|Type[NonNormalizedVersion]" + parent: Optional[_t.PathT] + _root: str + _relative_to: Optional[str] + version_cls: Type[VersionT] def __init__( self, relative_to: "_t.PathT | None" = None, root: _t.PathT = ".", - version_scheme: str = DEFAULT_VERSION_SCHEME, - local_scheme=DEFAULT_LOCAL_SCHEME, + version_scheme: Union[ + str, Callable[["ScmVersion"], Optional[str]] + ] = DEFAULT_VERSION_SCHEME, + local_scheme: Union[ + str, Callable[["ScmVersion"], Optional[str]] + ] = DEFAULT_LOCAL_SCHEME, write_to: "_t.PathT | None" = None, write_to_template: "str|None" = None, - tag_regex=DEFAULT_TAG_REGEX, - parentdir_prefix_version=None, + tag_regex: "str | Pattern[str]" = DEFAULT_TAG_REGEX, + parentdir_prefix_version: "str|None" = None, fallback_version: "str|None" = None, fallback_root: _t.PathT = ".", - parse=None, - git_describe_command=None, + parse: Optional[Any] = None, + git_describe_command: Optional[_t.CMD_TYPE] = None, dist_name: "str|None" = None, - version_cls: "Type[Version]|Type[NonNormalizedVersion]|str|None" = None, + version_cls: "Type[Version]|Type[NonNormalizedVersion]|type|str|None" = None, normalize: bool = True, search_parent_directories: bool = False, ): # TODO: - self._relative_to = relative_to + self._relative_to = None if relative_to is None else os.fspath(relative_to) self._root = "." - self.root = root + self.root = os.fspath(root) self.version_scheme = version_scheme self.local_scheme = local_scheme self.write_to = write_to self.write_to_template = write_to_template self.parentdir_prefix_version = parentdir_prefix_version self.fallback_version = fallback_version - self.fallback_root = fallback_root + self.fallback_root = fallback_root # type: ignore self.parse = parse - self.tag_regex = tag_regex + self.tag_regex = tag_regex # type: ignore self.git_describe_command = git_describe_command self.dist_name = dist_name self.search_parent_directories = search_parent_directories @@ -135,55 +150,55 @@ def __init__( self.version_cls = version_cls @property - def fallback_root(self): + def fallback_root(self) -> str: return self._fallback_root @fallback_root.setter - def fallback_root(self, value): + def fallback_root(self, value: _t.PathT) -> None: self._fallback_root = os.path.abspath(value) @property - def absolute_root(self): + def absolute_root(self) -> str: return self._absolute_root @property - def relative_to(self): + def relative_to(self) -> Optional[str]: return self._relative_to @relative_to.setter - def relative_to(self, value): + def relative_to(self, value: _t.PathT) -> None: self._absolute_root = _check_absolute_root(self._root, value) - self._relative_to = value + self._relative_to = os.fspath(value) trace("root", repr(self._absolute_root)) trace("relative_to", repr(value)) @property - def root(self): + def root(self) -> str: return self._root @root.setter - def root(self, value): + def root(self, value: _t.PathT) -> None: self._absolute_root = _check_absolute_root(value, self._relative_to) - self._root = value + self._root = os.fspath(value) trace("root", repr(self._absolute_root)) trace("relative_to", repr(self._relative_to)) @property - def tag_regex(self): + def tag_regex(self) -> Pattern[str]: return self._tag_regex @tag_regex.setter - def tag_regex(self, value): + def tag_regex(self, value: Union[str, Pattern[str]]) -> None: self._tag_regex = _check_tag_regex(value) @classmethod def from_file( cls, name: str = "pyproject.toml", - dist_name=None, # type: str | None - _load_toml=_lazy_tomli_load, - **kwargs, - ): + dist_name: Optional[str] = None, + _load_toml: Callable[[str], Dict[str, Any]] = _lazy_tomli_load, + **kwargs: Any, + ) -> "Configuration": """ Read Configuration from pyproject.toml (or similar). Raises exceptions when file is not found or toml is @@ -216,7 +231,7 @@ def from_file( return cls(dist_name=dist_name, **section, **kwargs) -def _read_dist_name_from_setup_cfg(): +def _read_dist_name_from_setup_cfg() -> Optional[str]: # minimal effort to read dist_name off setup.cfg metadata import configparser diff --git a/src/setuptools_scm/discover.py b/src/setuptools_scm/discover.py index f2aee17a..84fd3efd 100644 --- a/src/setuptools_scm/discover.py +++ b/src/setuptools_scm/discover.py @@ -1,11 +1,15 @@ import os +from typing import Iterable +from typing import Iterator +from . import _types as _t from .config import Configuration -from .utils import iter_entry_points from .utils import trace -def walk_potential_roots(root, search_parents=True): +def walk_potential_roots( + root: _t.PathT, search_parents: bool = True +) -> Iterator[_t.PathT]: """ Iterate though a path and each of its parents. :param root: File path. @@ -23,7 +27,7 @@ def walk_potential_roots(root, search_parents=True): root, tail = os.path.split(root) -def match_entrypoint(root, name): +def match_entrypoint(root: _t.PathT, name: str) -> bool: """ Consider a ``root`` as entry-point. :param root: File path. @@ -39,7 +43,9 @@ def match_entrypoint(root, name): return False -def iter_matching_entrypoints(root, entrypoint, config: Configuration): +def iter_matching_entrypoints( + root: _t.PathT, entrypoint: str, config: Configuration +) -> Iterable[_t.EntrypointProtocol]: """ Consider different entry-points in ``root`` and optionally its parents. :param root: File path. @@ -49,6 +55,7 @@ def iter_matching_entrypoints(root, entrypoint, config: Configuration): """ trace("looking for ep", 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): diff --git a/src/setuptools_scm/file_finder.py b/src/setuptools_scm/file_finder.py index 46660248..741cb9eb 100644 --- a/src/setuptools_scm/file_finder.py +++ b/src/setuptools_scm/file_finder.py @@ -1,9 +1,18 @@ +from __future__ import annotations # type: ignore + import os +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing_extensions import TypeGuard +from . import _types as _t from .utils import trace -def scm_find_files(path, scm_files, scm_dirs): +def scm_find_files( + path: _t.PathT, scm_files: set[str], scm_dirs: set[str] +) -> list[str]: """ setuptools compatible file finder that follows symlinks - path: the root directory from which to search @@ -19,13 +28,13 @@ def scm_find_files(path, scm_files, scm_dirs): adding-support-for-revision-control-systems """ realpath = os.path.normcase(os.path.realpath(path)) - seen = set() - res = [] + seen: set[str] = set() + res: list[str] = [] for dirpath, dirnames, filenames in os.walk(realpath, followlinks=True): # dirpath with symlinks resolved realdirpath = os.path.normcase(os.path.realpath(dirpath)) - def _link_not_in_scm(n): + def _link_not_in_scm(n: str) -> bool: fn = os.path.join(realdirpath, os.path.normcase(n)) return os.path.islink(fn) and fn not in scm_files @@ -57,7 +66,7 @@ def _link_not_in_scm(n): return res -def is_toplevel_acceptable(toplevel): +def is_toplevel_acceptable(toplevel: str | None) -> TypeGuard[str]: """ """ if toplevel is None: return False diff --git a/src/setuptools_scm/file_finder_git.py b/src/setuptools_scm/file_finder_git.py index c6f96d8a..922369a8 100644 --- a/src/setuptools_scm/file_finder_git.py +++ b/src/setuptools_scm/file_finder_git.py @@ -2,7 +2,13 @@ import os import subprocess import tarfile +from typing import IO +from typing import List +from typing import Optional +from typing import Set +from typing import Tuple +from . import _types as _t from .file_finder import is_toplevel_acceptable from .file_finder import scm_find_files from .utils import do_ex @@ -11,7 +17,7 @@ log = logging.getLogger(__name__) -def _git_toplevel(path): +def _git_toplevel(path: str) -> Optional[str]: try: cwd = os.path.abspath(path or ".") out, err, ret = do_ex(["git", "rev-parse", "HEAD"], cwd=cwd) @@ -48,7 +54,7 @@ def _git_toplevel(path): return None -def _git_interpret_archive(fd, toplevel): +def _git_interpret_archive(fd: IO[bytes], toplevel: str) -> Tuple[Set[str], Set[str]]: with tarfile.open(fileobj=fd, mode="r|*") as tf: git_files = set() git_dirs = {toplevel} @@ -61,7 +67,7 @@ def _git_interpret_archive(fd, toplevel): return git_files, git_dirs -def _git_ls_files_and_dirs(toplevel): +def _git_ls_files_and_dirs(toplevel: str) -> Tuple[Set[str], Set[str]]: # use git archive instead of git ls-file to honor # export-ignore git attribute @@ -69,6 +75,7 @@ def _git_ls_files_and_dirs(toplevel): proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, cwd=toplevel, stderr=subprocess.DEVNULL ) + assert proc.stdout is not None try: try: return _git_interpret_archive(proc.stdout, toplevel) @@ -79,13 +86,14 @@ def _git_ls_files_and_dirs(toplevel): except Exception: if proc.wait() != 0: log.error("listing git files failed - pretending there aren't any") - return (), () + return set(), set() -def git_find_files(path=""): - toplevel = _git_toplevel(path) +def git_find_files(path: _t.PathT = "") -> List[str]: + toplevel = _git_toplevel(os.fspath(path)) if not is_toplevel_acceptable(toplevel): return [] + assert toplevel is not None # mypy ignores typeguard fullpath = os.path.abspath(os.path.normpath(path)) if not fullpath.startswith(toplevel): trace("toplevel mismatch", toplevel, fullpath) diff --git a/src/setuptools_scm/file_finder_hg.py b/src/setuptools_scm/file_finder_hg.py index 53878c6a..35231793 100644 --- a/src/setuptools_scm/file_finder_hg.py +++ b/src/setuptools_scm/file_finder_hg.py @@ -1,12 +1,16 @@ import os import subprocess +from typing import List +from typing import Optional +from typing import Set +from typing import Tuple from .file_finder import is_toplevel_acceptable from .file_finder import scm_find_files from .utils import do_ex -def _hg_toplevel(path): +def _hg_toplevel(path: str) -> Optional[str]: try: with open(os.devnull, "wb") as devnull: out = subprocess.check_output( @@ -24,8 +28,8 @@ def _hg_toplevel(path): return None -def _hg_ls_files_and_dirs(toplevel): - hg_files = set() +def _hg_ls_files_and_dirs(toplevel: str) -> Tuple[Set[str], Set[str]]: + hg_files: Set[str] = set() hg_dirs = {toplevel} out, err, ret = do_ex(["hg", "files"], cwd=toplevel) if ret: @@ -41,9 +45,10 @@ def _hg_ls_files_and_dirs(toplevel): return hg_files, hg_dirs -def hg_find_files(path=""): +def hg_find_files(path: str = "") -> List[str]: toplevel = _hg_toplevel(path) if not is_toplevel_acceptable(toplevel): return [] + assert toplevel is not None hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel) return scm_find_files(path, hg_files, hg_dirs) diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 1566a12f..875d80a9 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -6,7 +6,15 @@ from os.path import isfile from os.path import join from os.path import samefile - +from typing import Callable +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple +from typing import TYPE_CHECKING +from typing import Union + +from . import _types as _t from .config import Configuration from .scm_workdir import Workdir from .utils import data_from_mime @@ -14,8 +22,12 @@ from .utils import require_command from .utils import trace from .version import meta +from .version import ScmVersion from .version import tags_to_versions +if TYPE_CHECKING: + from setuptools_scm.hg_git import GitWorkdirHgClient + REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b") DESCRIBE_UNSUPPORTED = "%(describe" @@ -39,13 +51,13 @@ class GitWorkdir(Workdir): COMMAND = "git" @classmethod - def from_potential_worktree(cls, wd): + def from_potential_worktree(cls, wd: _t.PathT) -> Optional["GitWorkdir"]: require_command(cls.COMMAND) wd = os.path.abspath(wd) real_wd, _, ret = do_ex("git rev-parse --show-prefix", wd) real_wd = real_wd[:-1] # remove the trailing pathsep if ret: - return + return None if not real_wd: real_wd = wd else: @@ -56,31 +68,31 @@ def from_potential_worktree(cls, wd): real_wd = wd[: -len(real_wd)] trace("real root", real_wd) if not samefile(real_wd, wd): - return + return None return cls(real_wd) - def is_dirty(self): + def is_dirty(self) -> bool: out, _, _ = self.do_ex("git status --porcelain --untracked-files=no") return bool(out) - def get_branch(self): + def get_branch(self) -> Optional[str]: 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) - branch = None + return None return branch - def get_head_date(self): + def get_head_date(self) -> Optional[date]: timestamp, err, ret = self.do_ex( "git -c log.showSignature=false log -n 1 HEAD --format=%cI" ) if ret: trace("timestamp err", timestamp, err, ret) - return + return None # TODO, when dropping python3.6 use fromiso date_part = timestamp.split("T")[0] if "%c" in date_part: @@ -88,39 +100,41 @@ def get_head_date(self): return None return datetime.strptime(date_part, r"%Y-%m-%d").date() - def is_shallow(self): + def is_shallow(self) -> bool: return isfile(join(self.path, ".git/shallow")) - def fetch_shallow(self): + def fetch_shallow(self) -> None: self.do_ex("git fetch --unshallow") - def node(self): + def node(self) -> Optional[str]: node, _, ret = self.do_ex("git rev-parse --verify --quiet HEAD") if not ret: return node[:7] + else: + return None - def count_all_nodes(self): + def count_all_nodes(self) -> int: revs, _, _ = self.do_ex("git rev-list HEAD") return revs.count("\n") + 1 - def default_describe(self): + def default_describe(self) -> _t.CmdResult: return self.do_ex(DEFAULT_DESCRIBE) -def warn_on_shallow(wd): +def warn_on_shallow(wd: GitWorkdir) -> None: """experimental, may change at any time""" if wd.is_shallow(): warnings.warn(f'"{wd.path}" is shallow and may cause errors') -def fetch_on_shallow(wd): +def fetch_on_shallow(wd: GitWorkdir) -> None: """experimental, may change at any time""" if wd.is_shallow(): warnings.warn(f'"{wd.path}" was shallow, git fetch was used to rectify') wd.fetch_shallow() -def fail_on_shallow(wd): +def fail_on_shallow(wd: GitWorkdir) -> None: """experimental, may change at any time""" if wd.is_shallow(): raise ValueError( @@ -128,7 +142,7 @@ def fail_on_shallow(wd): ) -def get_working_directory(config): +def get_working_directory(config: Configuration) -> Optional[GitWorkdir]: """ Return the working directory (``GitWorkdir``). """ @@ -142,7 +156,12 @@ def get_working_directory(config): return GitWorkdir.from_potential_worktree(config.absolute_root) -def parse(root, describe_command=None, pre_parse=warn_on_shallow, config=None): +def parse( + root: str, + describe_command: Optional[Union[str, List[str]]] = None, + pre_parse: Callable[[GitWorkdir], None] = warn_on_shallow, + config: Optional[Configuration] = None, +) -> Optional[ScmVersion]: """ :param pre_parse: experimental pre_parse action, may change at any time """ @@ -154,9 +173,18 @@ def parse(root, describe_command=None, pre_parse=warn_on_shallow, config=None): return _git_parse_inner( config, wd, describe_command=describe_command, pre_parse=pre_parse ) + else: + return None -def _git_parse_inner(config, wd, pre_parse=None, describe_command=None): +def _git_parse_inner( + config: Configuration, + wd: Union[GitWorkdir, "GitWorkdirHgClient"], + pre_parse: Optional[ + Callable[[Union[GitWorkdir, "GitWorkdirHgClient"]], None] + ] = None, + describe_command: Optional[_t.CMD_TYPE] = None, +) -> ScmVersion: if pre_parse: pre_parse(wd) @@ -167,7 +195,8 @@ def _git_parse_inner(config, wd, pre_parse=None, describe_command=None): out, _, ret = wd.do_ex(describe_command) else: out, _, ret = wd.default_describe() - + distance: Optional[int] + node: Optional[str] if ret == 0: tag, distance, node, dirty = _git_parse_describe(out) if distance == 0 and not dirty: @@ -197,7 +226,7 @@ def _git_parse_inner(config, wd, pre_parse=None, describe_command=None): ) -def _git_parse_describe(describe_output): +def _git_parse_describe(describe_output: str) -> Tuple[str, int, str, bool]: # 'describe_output' looks e.g. like 'v1.5.0-0-g4060507' or # 'v1.15.1rc1-37-g9bd1298-dirty'. @@ -208,11 +237,10 @@ def _git_parse_describe(describe_output): dirty = False tag, number, node = describe_output.rsplit("-", 2) - number = int(number) - return tag, number, node, dirty + return tag, int(number), node, dirty -def search_parent(dirname): +def search_parent(dirname: _t.PathT) -> Optional[GitWorkdir]: """ Walk up the path to find the `.git` directory. :param dirname: Directory from which to start searching. @@ -237,9 +265,12 @@ def search_parent(dirname): if not tail: return None + return None -def archival_to_version(data, config=None): +def archival_to_version( + data: Dict[str, str], config: Optional[Configuration] = None +) -> ScmVersion: trace("data", data) archival_describe = data.get("describe-name", DESCRIBE_UNSUPPORTED) if DESCRIBE_UNSUPPORTED in archival_describe: @@ -259,7 +290,9 @@ def archival_to_version(data, config=None): return meta("0.0", node=data.get("node"), config=config) -def parse_archival(root, config=None): +def parse_archival( + root: _t.PathT, config: Optional[Configuration] = None +) -> Optional[ScmVersion]: archival = os.path.join(root, ".git_archival.txt") data = data_from_mime(archival) return archival_to_version(data, config=config) diff --git a/src/setuptools_scm/hacks.py b/src/setuptools_scm/hacks.py index 3108de8b..259bab27 100644 --- a/src/setuptools_scm/hacks.py +++ b/src/setuptools_scm/hacks.py @@ -1,33 +1,45 @@ import os +from typing import Optional +from . import _types as _t +from .config import Configuration from .utils import data_from_mime from .utils import trace from .version import meta +from .version import ScmVersion from .version import tag_to_version +_UNKNOWN = "UNKNOWN" -def parse_pkginfo(root, config=None): + +def parse_pkginfo( + root: _t.PathT, config: Optional[Configuration] = None +) -> Optional[ScmVersion]: pkginfo = os.path.join(root, "PKG-INFO") trace("pkginfo", pkginfo) data = data_from_mime(pkginfo) - version = data.get("Version") - if version != "UNKNOWN": + version = data.get("Version", _UNKNOWN) + if version != _UNKNOWN: return meta(version, preformatted=True, config=config) + else: + return None -def parse_pip_egg_info(root, config=None): +def parse_pip_egg_info( + root: _t.PathT, config: Optional[Configuration] = None +) -> Optional[ScmVersion]: pipdir = os.path.join(root, "pip-egg-info") if not os.path.isdir(pipdir): - return + return None items = os.listdir(pipdir) trace("pip-egg-info", pipdir, items) if not items: - return + return None return parse_pkginfo(os.path.join(pipdir, items[0]), config=config) -def fallback_version(root, config=None): +def fallback_version(root: _t.PathT, config: Configuration) -> Optional[ScmVersion]: if config.parentdir_prefix_version is not None: _, parent_name = os.path.split(os.path.abspath(root)) if parent_name.startswith(config.parentdir_prefix_version): @@ -39,3 +51,4 @@ def fallback_version(root, config=None): if config.fallback_version is not None: trace("FALLBACK") return meta(config.fallback_version, preformatted=True, config=config) + return None diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index 8166a907..fad874be 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -1,6 +1,11 @@ +import datetime import os from pathlib import Path +from typing import Dict +from typing import Optional +from . import _types as _t +from ._version_cls import Version from .config import Configuration from .scm_workdir import Workdir from .utils import data_from_mime @@ -8,6 +13,7 @@ from .utils import require_command from .utils import trace from .version import meta +from .version import ScmVersion from .version import tag_to_version @@ -16,16 +22,20 @@ class HgWorkdir(Workdir): COMMAND = "hg" @classmethod - def from_potential_worktree(cls, wd): + def from_potential_worktree(cls, wd: _t.PathT) -> "HgWorkdir | None": require_command(cls.COMMAND) root, err, ret = do_ex("hg root", wd) if ret: - return + return None return cls(root) - def get_meta(self, config): + def get_meta(self, config: Configuration) -> Optional[ScmVersion]: - node, tags, bookmark, node_date = self.hg_log( + node: str + tags_str: str + bookmark: str + node_date_str: str + node, tags_str, bookmark, node_date_str = self.hg_log( ".", "{node}\n{tag}\n{bookmark}\n{date|shortdate}" ).split("\n") @@ -33,41 +43,48 @@ def get_meta(self, config): # mainly used to emulate Git branches, which is already supported with # the dedicated class GitWorkdirHgClient) - branch, dirty, dirty_date = self.do( + branch, dirty_str, dirty_date = self.do( ["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"] ).split("\n") - dirty = bool(int(dirty)) - - if dirty: - date = dirty_date - else: - date = node_date + dirty = bool(int(dirty_str)) + # todo: fromiso + node_date = datetime.date( + *map(int, (dirty_date if dirty else node_date_str).split("-")) + ) - if all(c == "0" for c in node): + if node.count("0") == len(node): trace("initial node", self.path) - return meta("0.0", config=config, dirty=dirty, branch=branch) + return meta( + "0.0", config=config, dirty=dirty, branch=branch, node_date=node_date + ) node = "h" + node[:7] - tags = tags.split() + tags = tags_str.split() if "tip" in tags: # tip is not a real tag - tags = tags.remove("tip") + tags.remove("tip") if tags: - tag = tags[0] - tag = tag_to_version(tag) + tag = tag_to_version(tags[0]) if tag: return meta(tag, dirty=dirty, branch=branch, config=config) try: - tag = self.get_latest_normalizable_tag() - dist = self.get_distance_revs(tag) - if tag == "null": - tag = "0.0" + tag_str = self.get_latest_normalizable_tag() + if tag_str is None: + dist = self.get_distance_revs("") + else: + dist = self.get_distance_revs(tag_str) + + if tag_str == "null" or tag_str is None: + tag = Version("0.0") dist = int(dist) + 1 + else: + tag = tag_to_version(tag_str, config=config) + assert tag is not None - if self.check_changes_since_tag(tag) or dirty: + if self.check_changes_since_tag(tag_str) or dirty: return meta( tag, distance=dist, @@ -75,37 +92,41 @@ def get_meta(self, config): dirty=dirty, branch=branch, config=config, - node_date=date, + node_date=node_date, ) else: - return meta(tag, config=config) + return meta(tag, config=config, node_date=node_date) - except ValueError: + except ValueError as e: + trace("error", e) pass # unpacking failed, old hg - def hg_log(self, revset, template): + return None + + def hg_log(self, revset: str, template: str) -> str: cmd = ["hg", "log", "-r", revset, "-T", template] return self.do(cmd) - def get_latest_normalizable_tag(self): + def get_latest_normalizable_tag(self) -> Optional[str]: # Gets all tags containing a '.' (see #229) from oldest to newest outlines = self.hg_log( revset="ancestors(.) and tag('re:\\.')", template="{tags}{if(tags, '\n', '')}", ).split() if not outlines: - return "null" + return None tag = outlines[-1].split()[-1] return tag - def get_distance_revs(self, rev1, rev2="."): + def get_distance_revs(self, rev1: str, rev2: str = ".") -> int: + revset = f"({rev1}::{rev2})" out = self.hg_log(revset, ".") return len(out) - 1 - def check_changes_since_tag(self, tag): + def check_changes_since_tag(self, tag: Optional[str]) -> bool: - if tag == "0.0": + if tag == "0.0" or tag is None: return True revset = ( @@ -119,7 +140,7 @@ def check_changes_since_tag(self, tag): return bool(self.hg_log(revset, ".")) -def parse(root, config=None): +def parse(root: _t.PathT, config: "Configuration|None" = None) -> Optional[ScmVersion]: if not config: config = Configuration(root=root) @@ -133,19 +154,21 @@ def parse(root, config=None): from .git import _git_parse_inner from .hg_git import GitWorkdirHgClient - wd = GitWorkdirHgClient.from_potential_worktree(root) - if wd: - return _git_parse_inner(config, wd) + wd_hggit = GitWorkdirHgClient.from_potential_worktree(root) + if wd_hggit: + return _git_parse_inner(config, wd_hggit) wd = HgWorkdir.from_potential_worktree(config.absolute_root) if wd is None: - return + return None return wd.get_meta(config) -def archival_to_version(data, config: "Configuration | None" = None): +def archival_to_version( + data: Dict[str, str], config: "Configuration | None" = None +) -> ScmVersion: trace("data", data) node = data.get("node", "")[:12] if node: @@ -155,7 +178,7 @@ def archival_to_version(data, config: "Configuration | None" = None): elif "latesttag" in data: return meta( data["latesttag"], - distance=data["latesttagdistance"], + distance=int(data["latesttagdistance"]), node=node, config=config, ) @@ -163,7 +186,9 @@ def archival_to_version(data, config: "Configuration | None" = None): return meta("0.0", node=node, config=config) -def parse_archival(root, config=None): +def parse_archival( + root: _t.PathT, config: Optional[Configuration] = None +) -> ScmVersion: archival = os.path.join(root, ".hg_archival.txt") data = data_from_mime(archival) return archival_to_version(data, config=config) diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index 323cdcb8..be843674 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -1,6 +1,11 @@ import os +from datetime import date from datetime import datetime +from typing import Dict +from typing import Optional +from typing import Set +from . import _types as _t from .git import GitWorkdir from .hg import HgWorkdir from .utils import do_ex @@ -8,47 +13,52 @@ from .utils import trace +_FAKE_GIT_DESCRIBE_ERROR = _t.CmdResult("<>hg git failed", "", 1) + + class GitWorkdirHgClient(GitWorkdir, HgWorkdir): COMMAND = "hg" @classmethod - def from_potential_worktree(cls, wd): + def from_potential_worktree(cls, wd: _t.PathT) -> "GitWorkdirHgClient | None": require_command(cls.COMMAND) - root, err, ret = do_ex("hg root", wd) + root, _, ret = do_ex("hg root", wd) if ret: - return + return None return cls(root) - def is_dirty(self): + def is_dirty(self) -> bool: out, _, _ = self.do_ex("hg id -T '{dirty}'") return bool(out) - def get_branch(self): - branch, err, ret = self.do_ex("hg id -T {bookmarks}") - if ret: - trace("branch err", branch, err, ret) - return - return branch + def get_branch(self) -> Optional[str]: + res = self.do_ex("hg id -T {bookmarks}") + if res.returncode: + trace("branch err", res) + return None + return res.out - def get_head_date(self): + def get_head_date(self) -> Optional[date]: date_part, err, ret = self.do_ex("hg log -r . -T {shortdate(date)}") if ret: trace("head date err", date_part, err, ret) - return + return None return datetime.strptime(date_part, r"%Y-%m-%d").date() - def is_shallow(self): + def is_shallow(self) -> bool: return False - def fetch_shallow(self): + def fetch_shallow(self) -> None: pass - def get_hg_node(self): + def get_hg_node(self) -> "str | None": node, _, ret = self.do_ex("hg log -r . -T {node}") if not ret: return node + else: + return None - def _hg2git(self, hg_node): + def _hg2git(self, hg_node: str) -> "str | None": git_node = None with open(os.path.join(self.path, ".hg/git-mapfile")) as file: for line in file: @@ -57,10 +67,10 @@ def _hg2git(self, hg_node): break return git_node - def node(self): + def node(self) -> "str | None": hg_node = self.get_hg_node() if hg_node is None: - return + return None git_node = self._hg2git(hg_node) @@ -80,18 +90,18 @@ def node(self): return git_node[:7] - def count_all_nodes(self): + def count_all_nodes(self) -> int: revs, _, _ = self.do_ex("hg log -r 'ancestors(.)' -T '.'") return len(revs) - def default_describe(self): + def default_describe(self) -> _t.CmdResult: """ Tentative to reproduce the output of `git describe --dirty --tags --long --match *[0-9]*` """ - hg_tags, _, ret = self.do_ex( + hg_tags_str, _, ret = self.do_ex( [ "hg", "log", @@ -102,32 +112,36 @@ def default_describe(self): ] ) if ret: - return None, None, None - hg_tags = hg_tags.split() + return _FAKE_GIT_DESCRIBE_ERROR + hg_tags: Set[str] = set(hg_tags_str.split()) if not hg_tags: - return None, None, None + return _FAKE_GIT_DESCRIBE_ERROR - git_tags = {} - with open(os.path.join(self.path, ".hg/git-tags")) as file: - for line in file: - node, tag = line.split() - git_tags[tag] = node + node: "str | None" = None - # find the first hg tag which is also a git tag - for tag in hg_tags: - if tag in git_tags: - break + with open(os.path.join(self.path, ".hg/git-tags")) as fp: + + git_tags: Dict[str, str] = dict(line.split() for line in fp) + + tag: "str | None" = next( + # find the first hg tag which is also a git tag + (tag for tag in hg_tags if tag in git_tags), + None, + ) + if tag is None: + return _FAKE_GIT_DESCRIBE_ERROR out, _, ret = self.do_ex(["hg", "log", "-r", f"'{tag}'::.", "-T", "."]) if ret: - return None, None, None + return _FAKE_GIT_DESCRIBE_ERROR distance = len(out) - 1 node = self.node() + assert node is not None desc = f"{tag}-{distance}-g{node}" if self.is_dirty(): desc += "-dirty" - return desc, None, 0 + return _t.CmdResult(desc, "", 0) diff --git a/src/setuptools_scm/integration.py b/src/setuptools_scm/integration.py index 7d399a33..53df404f 100644 --- a/src/setuptools_scm/integration.py +++ b/src/setuptools_scm/integration.py @@ -1,18 +1,23 @@ import os import warnings +from typing import Any +from typing import Callable +from typing import Dict +from typing import List import setuptools from . import _get_version +from . import _types as _t from . import _version_missing +from ._entrypoints import iter_entry_points from .config import _read_dist_name_from_setup_cfg from .config import Configuration from .utils import do -from .utils import iter_entry_points from .utils import trace -def _warn_on_old_setuptools(_version=setuptools.__version__): +def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: if int(_version.split(".")[0]) < 45: warnings.warn( RuntimeWarning( @@ -45,7 +50,7 @@ def _warn_on_old_setuptools(_version=setuptools.__version__): _warn_on_old_setuptools() -def _assign_version(dist: setuptools.Distribution, config: Configuration): +def _assign_version(dist: setuptools.Distribution, config: Configuration) -> None: maybe_version = _get_version(config) if maybe_version is None: @@ -54,12 +59,16 @@ def _assign_version(dist: setuptools.Distribution, config: Configuration): dist.metadata.version = maybe_version -def version_keyword(dist: setuptools.Distribution, keyword, value): +def version_keyword( + dist: setuptools.Distribution, + keyword: str, + value: "bool | Dict[str, Any] | Callable[[], Dict[str, Any]]", +) -> None: if not value: return - if value is True: + elif value is True: value = {} - if getattr(value, "__call__", None): + elif callable(value): value = value() assert ( "dist_name" not in value @@ -76,7 +85,7 @@ def version_keyword(dist: setuptools.Distribution, keyword, value): _assign_version(dist, config) -def find_files(path=""): +def find_files(path: _t.PathT = "") -> List[str]: for ep in iter_entry_points("setuptools_scm.files_command"): command = ep.load() if isinstance(command, str): @@ -89,7 +98,7 @@ def find_files(path=""): return [] -def infer_version(dist: setuptools.Distribution): +def infer_version(dist: setuptools.Distribution) -> None: trace( "finalize hook", vars(dist.metadata), diff --git a/src/setuptools_scm/scm_workdir.py b/src/setuptools_scm/scm_workdir.py index 142065f5..2d5dc173 100644 --- a/src/setuptools_scm/scm_workdir.py +++ b/src/setuptools_scm/scm_workdir.py @@ -1,15 +1,20 @@ +from typing import ClassVar + +from . import _types as _t from .utils import do from .utils import do_ex from .utils import require_command class Workdir: - def __init__(self, path): + COMMAND: ClassVar[str] + + def __init__(self, path: _t.PathT): require_command(self.COMMAND) self.path = path - def do_ex(self, cmd): + def do_ex(self, cmd: _t.CMD_TYPE) -> _t.CmdResult: return do_ex(cmd, cwd=self.path) - def do(self, cmd): + def do(self, cmd: _t.CMD_TYPE) -> str: return do(cmd, cwd=self.path) diff --git a/src/setuptools_scm/utils.py b/src/setuptools_scm/utils.py index 744cade8..73a988c8 100644 --- a/src/setuptools_scm/utils.py +++ b/src/setuptools_scm/utils.py @@ -8,10 +8,12 @@ import subprocess import sys import warnings -from typing import cast +from os import _Environ from typing import Dict +from typing import Iterator from typing import List -from typing import Tuple +from typing import Optional +from typing import Union from . import _types as _t @@ -19,7 +21,7 @@ IS_WINDOWS = platform.system() == "Windows" -def no_git_env(env): +def no_git_env(env: Union[Dict[str, str], _Environ[str]]) -> Dict[str, str]: # adapted from pre-commit # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 @@ -40,7 +42,7 @@ def no_git_env(env): } -def trace(*k) -> None: +def trace(*k: object) -> None: if DEBUG: print(*k, file=sys.stderr, flush=True) @@ -52,36 +54,24 @@ def ensure_stripped_str(str_or_bytes: "str | bytes") -> str: return str_or_bytes.decode("utf-8", "surrogateescape").strip() -def _always_strings(env_dict: dict) -> Dict[str, str]: - """ - On Windows and Python 2, environment dictionaries must be strings - and not unicode. - """ - if IS_WINDOWS: - env_dict.update((key, str(value)) for (key, value) in env_dict.items()) - return cast(Dict[str, str], env_dict) - - -def _popen_pipes(cmd, cwd: "str | _t.PathT"): +def _popen_pipes(cmd: _t.CMD_TYPE, cwd: _t.PathT) -> subprocess.Popen[bytes]: return subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=str(cwd), - env=_always_strings( - dict( - no_git_env(os.environ), - # os.environ, - # try to disable i18n - LC_ALL="C", - LANGUAGE="", - HGPLAIN="1", - ) + env=dict( + no_git_env(os.environ), + # os.environ, + # try to disable i18n + LC_ALL="C", + LANGUAGE="", + HGPLAIN="1", ), ) -def do_ex(cmd, cwd: "str | _t.PathT" = ".") -> Tuple[str, str, int]: +def do_ex(cmd: _t.CMD_TYPE, cwd: _t.PathT = ".") -> _t.CmdResult: trace("cmd", repr(cmd)) trace(" in", cwd) if os.name == "posix" and not isinstance(cmd, (list, tuple)): @@ -95,10 +85,12 @@ def do_ex(cmd, cwd: "str | _t.PathT" = ".") -> Tuple[str, str, int]: trace("err", repr(err)) if p.returncode: trace("ret", p.returncode) - return ensure_stripped_str(out), ensure_stripped_str(err), p.returncode + return _t.CmdResult( + ensure_stripped_str(out), ensure_stripped_str(err), p.returncode + ) -def do(cmd: "List[str] | str", cwd: "str | _t.PathT" = "."): +def do(cmd: "List[str] | str", cwd: "str | _t.PathT" = ".") -> str: out, err, ret = do_ex(cmd, cwd) if ret: print(err) @@ -138,13 +130,15 @@ def has_command(name: str, args: "List[str] | None" = None, warn: bool = True) - return res -def require_command(name): +def require_command(name: str) -> None: if not has_command(name, warn=False): raise OSError("%r was not found" % name) -def iter_entry_points(*k, **kw): +def iter_entry_points( + group: str, name: Optional[str] = None +) -> Iterator[_t.EntrypointProtocol]: from ._entrypoints import iter_entry_points - return iter_entry_points(*k, **kw) + return iter_entry_points(group, name) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index faaf8fa4..f7796649 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -1,31 +1,44 @@ +from __future__ import annotations # type: ignore + import os import re import warnings +from datetime import date from datetime import datetime from datetime import timezone +from typing import Any from typing import Callable +from typing import cast from typing import Iterator from typing import List +from typing import Match from typing import overload from typing import Tuple +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing_extensions import Concatenate +from . import _types as _t +from ._version_cls import Version as PkgVersion from .config import Configuration -from .config import Version as PkgVersion -from .utils import iter_entry_points +from .config import VersionT from .utils import trace - SEMVER_MINOR = 2 SEMVER_PATCH = 3 SEMVER_LEN = 3 -def _parse_version_tag(tag, config): +def _parse_version_tag( + tag: str | object, config: Configuration +) -> dict[str, str] | None: tagstring = tag if isinstance(tag, str) else str(tag) match = config.tag_regex.match(tagstring) result = None if match: + key: str | int if len(match.groups()) == 1: key = 1 else: @@ -41,18 +54,21 @@ def _parse_version_tag(tag, config): return result -def callable_or_entrypoint(group, callable_or_name): +def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any: trace("ep", (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) return ep.load() -def tag_to_version(tag, config: "Configuration | None" = None): +def tag_to_version( + tag: VersionT | str, config: Configuration | None = None +) -> VersionT | None: """ take a tag that might be prefixed with a keyword and return only the version part :param config: optional configuration object @@ -67,8 +83,8 @@ def tag_to_version(tag, config: "Configuration | None" = None): warnings.warn(f"tag {tag!r} no version found") return None - version = tagdict["version"] - trace("version pre parse", version) + version_str = tagdict["version"] + trace("version pre parse", version_str) if tagdict.get("suffix", ""): warnings.warn( @@ -77,38 +93,40 @@ def tag_to_version(tag, config: "Configuration | None" = None): ) ) - version = config.version_cls(version) + version = config.version_cls(version_str) trace("version", repr(version)) return version -def tags_to_versions(tags, config=None): +def tags_to_versions( + tags: list[str], config: Configuration | None = None +) -> list[VersionT]: """ take tags that might be prefixed with a keyword and return only the version part :param tags: an iterable of tags :param config: optional configuration object """ - result = [] + result: list[VersionT] = [] for tag in tags: - tag = tag_to_version(tag, config=config) - if tag: - result.append(tag) + parsed = tag_to_version(tag, config=config) + if parsed: + result.append(parsed) return result class ScmVersion: def __init__( self, - tag_version: str, - distance: "int|None" = None, - node: "str|None" = None, + tag_version: Any, + config: Configuration, + distance: int | None = None, + node: str | None = None, dirty: bool = False, preformatted: bool = False, - branch: "str|None" = None, - config: "Configuration|None" = None, - node_date=None, - **kw, + branch: str | None = None, + node_date: date | None = None, + **kw: object, ): if kw: trace("unknown args", kw) @@ -130,7 +148,7 @@ def __init__( self.config = config @property - def extra(self): + def extra(self) -> dict[str, Any]: warnings.warn( "ScmVersion.extra is deprecated and will be removed in future", category=DeprecationWarning, @@ -139,15 +157,15 @@ def extra(self): return self._extra @property - def exact(self): + def exact(self) -> bool: return self.distance is None - def __repr__(self): + def __repr__(self) -> str: return self.format_with( "" ) - def format_with(self, fmt, **kw): + def format_with(self, fmt: str, **kw: object) -> str: return fmt.format( time=self.time, tag=self.tag, @@ -159,52 +177,73 @@ def format_with(self, fmt, **kw): **kw, ) - def format_choice(self, clean_format, dirty_format, **kw): + def format_choice(self, clean_format: str, dirty_format: str, **kw: object) -> str: return self.format_with(dirty_format if self.dirty else clean_format, **kw) - def format_next_version(self, guess_next, fmt="{guessed}.dev{distance}", **kw): - guessed = guess_next(self.tag, **kw) + def format_next_version( + self, + guess_next: Callable[Concatenate[ScmVersion, _t.P], str], + fmt: str = "{guessed}.dev{distance}", + *k: _t.P.args, # type: ignore + **kw: _t.P.kwargs, # type: ignore + ) -> str: + guessed = guess_next(self, *k, **kw) return self.format_with(fmt, guessed=guessed) -def _parse_tag(tag, preformatted, config: "Configuration|None"): +def _parse_tag( + tag: VersionT | str, preformatted: bool, config: Configuration | None +) -> VersionT | str: if preformatted: return tag - if config is None or not isinstance(tag, config.version_cls): - tag = tag_to_version(tag, config) - return tag + elif config is None or not isinstance(tag, config.version_cls): + version = tag_to_version(tag, config) + assert version is not None + return version + else: + return tag def meta( - tag, - distance: "int|None" = None, + tag: str | VersionT, + distance: int | None = None, dirty: bool = False, - node: "str|None" = None, + node: str | None = None, preformatted: bool = False, - branch: "str|None" = None, - config: "Configuration|None" = None, - **kw, + branch: str | None = None, + config: Configuration | None = None, + node_date: date | None = None, + **kw: Any, ) -> ScmVersion: if not config: warnings.warn( "meta invoked without explicit configuration," " will use defaults where required." ) + config = Configuration() parsed_version = _parse_tag(tag, preformatted, config) trace("version", tag, "->", parsed_version) assert parsed_version is not None, "Can't parse version %s" % tag return ScmVersion( - parsed_version, distance, node, dirty, preformatted, branch, config, **kw + parsed_version, + distance=distance, + node=node, + dirty=dirty, + preformatted=preformatted, + branch=branch, + config=config, + node_date=node_date, + **kw, ) -def guess_next_version(tag_version: ScmVersion): - version = _strip_local(str(tag_version)) +def guess_next_version(tag_version: ScmVersion) -> str: + version = _strip_local(str(tag_version.tag)) return _bump_dev(version) or _bump_regex(version) -def _dont_guess_next_version(tag_version: ScmVersion): - version = _strip_local(str(tag_version)) +def _dont_guess_next_version(tag_version: ScmVersion) -> str: + version = _strip_local(str(tag_version.tag)) return _bump_dev(version) or _add_post(version) @@ -213,7 +252,7 @@ def _strip_local(version_string: str) -> str: return public -def _add_post(version: str): +def _add_post(version: str) -> str: if "post" in version: raise ValueError( f"{version} already is a post release, refusing to guess the update" @@ -221,7 +260,7 @@ def _add_post(version: str): return f"{version}.post1" -def _bump_dev(version: str) -> "str | None": +def _bump_dev(version: str) -> str | None: if ".dev" not in version: return None @@ -248,16 +287,18 @@ def _bump_regex(version: str) -> str: return "%s%d" % (prefix, int(tail) + 1) -def guess_next_dev_version(version: ScmVersion): +def guess_next_dev_version(version: ScmVersion) -> str: if version.exact: return version.format_with("{tag}") else: return version.format_next_version(guess_next_version) -def guess_next_simple_semver(version: str, retain: int, increment=True): +def guess_next_simple_semver( + version: ScmVersion, retain: int, increment: bool = True +) -> str: try: - parts = [int(i) for i in str(version).split(".")[:retain]] + parts = [int(i) for i in str(version.tag).split(".")[:retain]] except ValueError: raise ValueError(f"{version} can't be parsed as numeric version") while len(parts) < retain: @@ -269,9 +310,9 @@ def guess_next_simple_semver(version: str, retain: int, increment=True): return ".".join(str(i) for i in parts) -def simplified_semver_version(version): +def simplified_semver_version(version: ScmVersion) -> str: if version.exact: - return guess_next_simple_semver(version.tag, retain=SEMVER_LEN, increment=False) + return guess_next_simple_semver(version, retain=SEMVER_LEN, increment=False) else: if version.branch is not None and "feature" in version.branch: return version.format_next_version( @@ -283,14 +324,16 @@ def simplified_semver_version(version): ) -def release_branch_semver_version(version): +def release_branch_semver_version(version: ScmVersion) -> str: if version.exact: return version.format_with("{tag}") if version.branch is not None: # Does the branch name (stripped of namespace) parse as a version? - branch_ver = _parse_version_tag(version.branch.split("/")[-1], version.config) - if branch_ver is not None: - branch_ver = branch_ver["version"] + branch_ver_data = _parse_version_tag( + version.branch.split("/")[-1], version.config + ) + if branch_ver_data is not None: + branch_ver = branch_ver_data["version"] if branch_ver[0] == "v": # Allow branches that start with 'v', similar to Version. branch_ver = branch_ver[1:] @@ -306,7 +349,7 @@ def release_branch_semver_version(version): return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR) -def release_branch_semver(version): +def release_branch_semver(version: ScmVersion) -> str: warnings.warn( "release_branch_semver is deprecated and will be removed in future. " + "Use release_branch_semver_version instead", @@ -316,32 +359,37 @@ def release_branch_semver(version): return release_branch_semver_version(version) -def no_guess_dev_version(version: ScmVersion): +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) -def date_ver_match(ver): +def date_ver_match(ver: str) -> Match[str] | None: match = re.match( ( r"^(?P(?P\d{2}|\d{4})(?:\.\d{1,2}){2})" r"(?:\.(?P\d*)){0,1}?$" ), - str(ver), + ver, ) return match -def guess_next_date_ver(version, node_date=None, date_fmt=None, version_cls=None): +def guess_next_date_ver( + version: ScmVersion, + node_date: date | None = None, + date_fmt: str | None = None, + version_cls: type | None = None, +) -> str: """ same-day -> patch +1 other-day -> today distance is always added as .devX """ - match = date_ver_match(version) + match = date_ver_match(str(version.tag)) if match is None: warnings.warn( f"{version} does not correspond to a valid versioning date, " @@ -349,10 +397,10 @@ def guess_next_date_ver(version, node_date=None, date_fmt=None, version_cls=None ) if date_fmt is None: date_fmt = "%y.%m.%d" - - # deduct date format if not provided - if date_fmt is None: - date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d" + else: + # deduct date format if not provided + if date_fmt is None: + date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d" today = datetime.now(timezone.utc).date() head_date = node_date or today # compute patch @@ -382,7 +430,7 @@ def guess_next_date_ver(version, node_date=None, date_fmt=None, version_cls=None return next_version -def calver_by_date(version): +def calver_by_date(version: ScmVersion) -> str: if version.exact and not version.dirty: return version.format_with("{tag}") # TODO: move the release-X check to a new scheme @@ -400,7 +448,7 @@ def calver_by_date(version): ) -def _format_local_with_time(version, time_format): +def _format_local_with_time(version: ScmVersion, time_format: str) -> str: if version.exact or version.node is None: return version.format_choice( @@ -412,42 +460,55 @@ def _format_local_with_time(version, time_format): ) -def get_local_node_and_date(version): +def get_local_node_and_date(version: ScmVersion) -> str: return _format_local_with_time(version, time_format="%Y%m%d") -def get_local_node_and_timestamp(version, fmt="%Y%m%d%H%M%S"): +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) -def get_local_dirty_tag(version): +def get_local_dirty_tag(version: ScmVersion) -> str: return version.format_choice("", "+dirty") -def get_no_local_node(_): +def get_no_local_node(_: Any) -> str: return "" -def postrelease_version(version): +def postrelease_version(version: ScmVersion) -> str: if version.exact: return version.format_with("{tag}") else: return version.format_with("{tag}.post{distance}") -def _get_ep(group, name): +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) return ep.load() + else: + return None def _iter_version_schemes( - entrypoint: str, scheme_value: "str|List[str]|Tuple[str, ...]", _memo=None -) -> Iterator[Callable[["ScmVersion"], str]]: + entrypoint: str, + scheme_value: str + | list[str] + | tuple[str, ...] + | Callable[[ScmVersion], str] + | None, + _memo: set[object] | None = None, +) -> Iterator[Callable[[ScmVersion], str]]: if _memo is None: _memo = set() if isinstance(scheme_value, str): - scheme_value = _get_ep(entrypoint, scheme_value) + scheme_value = cast( + 'str|List[str]|Tuple[str, ...]|Callable[["ScmVersion"], str]|None', + _get_ep(entrypoint, scheme_value), + ) if isinstance(scheme_value, (list, tuple)): for variant in scheme_value: @@ -468,13 +529,13 @@ def _call_version_scheme( @overload def _call_version_scheme( version: ScmVersion, entypoint: str, given_value: str, default: None -) -> "str|None": +) -> str | None: ... def _call_version_scheme( - version: ScmVersion, entypoint: str, given_value: str, default: "str|None" -) -> "str|None": + version: ScmVersion, entypoint: str, given_value: str, default: str | None +) -> str | None: for scheme in _iter_version_schemes(entypoint, given_value): result = scheme(version) if result is not None: @@ -482,10 +543,11 @@ def _call_version_scheme( return default -def format_version(version: ScmVersion, **config) -> str: +def format_version(version: ScmVersion, **config: Any) -> str: trace("scm version", version) trace("config", config) if version.preformatted: + assert isinstance(version.tag, str) return version.tag main_version = _call_version_scheme( version, "setuptools_scm.version_scheme", config["version_scheme"], None diff --git a/testing/conftest.py b/testing/conftest.py index 7691215e..ab9b6417 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,4 +1,9 @@ import os +from pathlib import Path +from typing import Any +from typing import Generator +from typing import List +from typing import Tuple import pytest @@ -11,11 +16,11 @@ VERSION_PKGS = ["setuptools", "setuptools_scm"] -def pytest_report_header(): +def pytest_report_header() -> List[str]: try: - from importlib.metadata import version + from importlib.metadata import version # type: ignore except ImportError: - from importlib_metadata import version + from importlib_metadata import version # type: ignore res = [] for pkg in VERSION_PKGS: pkg_version = version(pkg) @@ -24,7 +29,7 @@ def pytest_report_header(): return res -def pytest_addoption(parser): +def pytest_addoption(parser: Any) -> None: group = parser.getgroup("setuptools_scm") group.addoption( "--test-legacy", dest="scm_test_virtualenv", default=False, action="store_true" @@ -32,7 +37,7 @@ def pytest_addoption(parser): @pytest.fixture(autouse=True) -def debug_mode(): +def debug_mode() -> Generator[None, None, None]: from setuptools_scm import utils utils.DEBUG = True @@ -41,14 +46,14 @@ def debug_mode(): @pytest.fixture -def wd(tmp_path): +def wd(tmp_path: Path) -> WorkDir: target_wd = tmp_path.resolve() / "wd" target_wd.mkdir() return WorkDir(target_wd) @pytest.fixture -def repositories_hg_git(tmp_path): +def repositories_hg_git(tmp_path: Path) -> Tuple[WorkDir, WorkDir]: from setuptools_scm.utils import do tmp_path = tmp_path.resolve() diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py index 02ea82fd..ef79fca1 100644 --- a/testing/test_basic_api.py +++ b/testing/test_basic_api.py @@ -7,27 +7,29 @@ import setuptools_scm from setuptools_scm import dump_version +from setuptools_scm.config import Configuration from setuptools_scm.utils import data_from_mime from setuptools_scm.utils import do from setuptools_scm.version import ScmVersion +from testing.wd_wrapper import WorkDir @pytest.mark.parametrize("cmd", ["ls", "dir"]) -def test_do(cmd, tmp_path: Path): +def test_do(cmd: str, tmp_path: Path) -> None: if not shutil.which(cmd): - pytest.skip(cmd + " not found") + pytest.skip(f"{cmd} not found") do(cmd, cwd=tmp_path) -def test_data_from_mime(tmpdir): - tmpfile = tmpdir.join("test.archival") - tmpfile.write("name: test\nrevision: 1") +def test_data_from_mime(tmp_path: Path) -> None: + tmpfile = tmp_path.joinpath("test.archival") + tmpfile.write_text("name: test\nrevision: 1") res = data_from_mime(str(tmpfile)) assert res == {"name": "test", "revision": "1"} -def test_version_from_pkginfo(wd, monkeypatch): +def test_version_from_pkginfo(wd: WorkDir) -> None: wd.write("PKG-INFO", "Version: 0.1") assert wd.version == "0.1" @@ -36,29 +38,31 @@ def test_version_from_pkginfo(wd, monkeypatch): assert wd.get_version(version_scheme="1.{0.distance}.0".format) == "0.1" -def assert_root(monkeypatch, expected_root): +def assert_root(monkeypatch: pytest.MonkeyPatch, expected_root: str) -> None: """ Patch version_from_scm to simply assert that root is expected root """ - def assertion(config): + def assertion(config: Configuration) -> ScmVersion: assert config.absolute_root == expected_root - return ScmVersion("1.0") + return ScmVersion("1.0", config=config) monkeypatch.setattr(setuptools_scm, "_do_parse", assertion) -def test_root_parameter_creation(monkeypatch): +def test_root_parameter_creation(monkeypatch: pytest.MonkeyPatch) -> None: assert_root(monkeypatch, os.getcwd()) setuptools_scm.get_version() -def test_version_from_scm(wd): +def test_version_from_scm(wd: WorkDir) -> None: with pytest.warns(DeprecationWarning, match=".*version_from_scm.*"): setuptools_scm.version_from_scm(str(wd)) -def test_root_parameter_pass_by(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): +def test_root_parameter_pass_by( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: assert_root(monkeypatch, os.fspath(tmp_path)) setuptools_scm.get_version(root=os.fspath(tmp_path)) setuptools_scm.get_version( @@ -66,66 +70,73 @@ def test_root_parameter_pass_by(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) ) # issue 669 - posarg difference between Configuration and get_version -def test_parentdir_prefix(tmpdir, monkeypatch): +def test_parentdir_prefix(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") - p = tmpdir.ensure("projectname-v12.34", dir=True) - p.join("setup.py").write( + p = tmp_path.joinpath("projectname-v12.34") + p.mkdir() + p.joinpath("setup.py").write_text( """from setuptools import setup setup(use_scm_version={"parentdir_prefix_version": "projectname-"}) """ ) - res = do((sys.executable, "setup.py", "--version"), p) + res = do([sys.executable, "setup.py", "--version"], p) assert res == "12.34" -def test_fallback(tmpdir, monkeypatch): +def test_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") - p = tmpdir.ensure("sub/package", dir=1) - p.join("setup.py").write( + p = tmp_path / "sub/package" + p.mkdir(parents=True) + p.joinpath("setup.py").write_text( """from setuptools import setup setup(use_scm_version={"fallback_version": "12.34"}) """ ) - res = do((sys.executable, "setup.py", "--version"), p) + res = do([sys.executable, "setup.py", "--version"], p) assert res == "12.34" -def test_empty_pretend_version(tmpdir, monkeypatch): +def test_empty_pretend_version(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION", "") - p = tmpdir.ensure("sub/package", dir=1) - p.join("setup.py").write( + p = tmp_path / "sub/package" + p.mkdir(parents=True) + p.joinpath("setup.py").write_text( """from setuptools import setup setup(use_scm_version={"fallback_version": "12.34"}) """ ) - res = do((sys.executable, "setup.py", "--version"), p) + res = do([sys.executable, "setup.py", "--version"], p) assert res == "12.34" -def test_empty_pretend_version_named(tmpdir, monkeypatch): +def test_empty_pretend_version_named( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION", "1.23") monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MYSCM", "") - p = tmpdir.ensure("sub/package", dir=1) - p.join("setup.py").write( + p = tmp_path.joinpath("sub/package") + p.mkdir(parents=True) + + p.joinpath("setup.py").write_text( """from setuptools import setup setup(name="myscm", use_scm_version={"fallback_version": "12.34"}) """ ) - res = do((sys.executable, "setup.py", "--version"), p) + res = do([sys.executable, "setup.py", "--version"], p) assert res == "12.34" @pytest.mark.parametrize( "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625", "2345"] ) -def test_pretended(version, monkeypatch): +def test_pretended(version: str, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv(setuptools_scm.PRETEND_KEY, version) assert setuptools_scm.get_version() == version -def test_root_relative_to(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): +def test_root_relative_to(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: tmp_path.joinpath("setup.cfg").touch() assert_root(monkeypatch, str(tmp_path / "alt")) module = tmp_path / "module/file.py" @@ -143,7 +154,7 @@ def test_root_relative_to(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): ) -def test_dump_version(tmp_path: Path): +def test_dump_version(tmp_path: Path) -> None: dump_version(tmp_path, "1.0", "first.txt") @@ -154,40 +165,43 @@ def read(name: str) -> str: dump_version(tmp_path, "1.0.dev42", "first.py") lines = read("first.py").splitlines() - assert "version = '1.0.dev42'" in lines - assert "version_tuple = (1, 0, 'dev42')" in lines + assert "__version__ = version = '1.0.dev42'" in lines + assert "__version_tuple__ = version_tuple = (1, 0, 'dev42')" in lines dump_version(tmp_path, "1.0.1+g4ac9d2c", "second.py") lines = read("second.py").splitlines() - assert "version = '1.0.1+g4ac9d2c'" in lines - assert "version_tuple = (1, 0, 1, 'g4ac9d2c')" in lines + assert "__version__ = version = '1.0.1+g4ac9d2c'" in lines + assert "__version_tuple__ = version_tuple = (1, 0, 1, 'g4ac9d2c')" in lines dump_version(tmp_path, "1.2.3.dev18+gb366d8b.d20210415", "third.py") lines = read("third.py").splitlines() - assert "version = '1.2.3.dev18+gb366d8b.d20210415'" in lines - assert "version_tuple = (1, 2, 3, 'dev18', 'gb366d8b.d20210415')" in lines + assert "__version__ = version = '1.2.3.dev18+gb366d8b.d20210415'" in lines + assert ( + "__version_tuple__ = version_tuple = (1, 2, 3, 'dev18', 'gb366d8b.d20210415')" + in lines + ) import ast ast.parse(read("third.py")) -def test_parse_plain_fails(recwarn): - def parse(root): +def test_parse_plain_fails(recwarn: pytest.WarningsRecorder) -> None: + def parse(root: object) -> str: return "tricked you" with pytest.raises(TypeError): setuptools_scm.get_version(parse=parse) -def test_custom_version_cls(): +def test_custom_version_cls() -> None: """Test that `normalize` and `version_cls` work as expected""" class MyVersion: def __init__(self, tag_str: str): self.version = tag_str - def __repr__(self): + def __repr__(self) -> str: return f"hello,{self.version}" # you can not use normalize=False and version_cls at the same time diff --git a/testing/test_config.py b/testing/test_config.py index b205681e..53c034a7 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,5 +1,6 @@ import re import textwrap +from pathlib import Path import pytest @@ -20,7 +21,7 @@ ("V1.1", "V1.1"), ], ) -def test_tag_regex(tag, expected_version): +def test_tag_regex(tag: str, expected_version: str) -> None: config = Configuration() match = config.tag_regex.match(tag) assert match @@ -28,8 +29,8 @@ def test_tag_regex(tag, expected_version): assert version == expected_version -def test_config_from_pyproject(tmpdir): - fn = tmpdir / "pyproject.toml" +def test_config_from_pyproject(tmp_path: Path) -> None: + fn = tmp_path / "pyproject.toml" fn.write_text( textwrap.dedent( """ @@ -44,7 +45,7 @@ def test_config_from_pyproject(tmpdir): assert Configuration.from_file(str(fn)) -def test_config_regex_init(): +def test_config_regex_init() -> None: tag_regex = re.compile(r"v(\d+)") conf = Configuration(tag_regex=tag_regex) assert conf.tag_regex is tag_regex diff --git a/testing/test_file_finder.py b/testing/test_file_finder.py index e469d00d..ec233499 100644 --- a/testing/test_file_finder.py +++ b/testing/test_file_finder.py @@ -1,14 +1,21 @@ import os import sys +from typing import Generator +from typing import Iterable +from typing import Set import pytest +from .wd_wrapper import WorkDir from setuptools_scm.integration import find_files @pytest.fixture(params=["git", "hg"]) -def inwd(request, wd, monkeypatch): - if request.param == "git": +def inwd( + request: pytest.FixtureRequest, wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> Generator[WorkDir, None, None]: + param: str = getattr(request, "param") # todo: fix + if param == "git": try: wd("git init") except OSError: @@ -17,7 +24,7 @@ def inwd(request, wd, monkeypatch): wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" - elif request.param == "hg": + elif param == "hg": try: wd("hg init") except OSError: @@ -37,23 +44,23 @@ def inwd(request, wd, monkeypatch): yield wd -def _sep(paths): +def _sep(paths: Iterable[str]) -> Set[str]: return {path.replace("/", os.path.sep) for path in paths} -def test_basic(inwd): +def test_basic(inwd: WorkDir) -> None: assert set(find_files()) == _sep({"file1", "adir/filea", "bdir/fileb"}) assert set(find_files(".")) == _sep({"./file1", "./adir/filea", "./bdir/fileb"}) assert set(find_files("adir")) == _sep({"adir/filea"}) -def test_whitespace(inwd): +def test_whitespace(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "space file").touch() inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/space file", "adir/filea"}) -def test_case(inwd): +def test_case(inwd: WorkDir) -> None: (inwd.cwd / "CamelFile").touch() (inwd.cwd / "file2").touch() inwd.add_and_commit() @@ -63,14 +70,14 @@ def test_case(inwd): @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") -def test_symlink_dir(inwd): +def test_symlink_dir(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir") inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea", "adir/bdirlink/fileb"}) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") -def test_symlink_dir_source_not_in_scm(inwd): +def test_symlink_dir_source_not_in_scm(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir") assert set(find_files("adir")) == _sep({"adir/filea"}) @@ -78,7 +85,7 @@ def test_symlink_dir_source_not_in_scm(inwd): @pytest.mark.skipif( sys.platform == "win32", reason="symlinks to files not supported on windows" ) -def test_symlink_file(inwd): +def test_symlink_file(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "file1link").symlink_to("../file1") inwd.add_and_commit() assert set(find_files("adir")) == _sep( @@ -89,20 +96,20 @@ def test_symlink_file(inwd): @pytest.mark.skipif( sys.platform == "win32", reason="symlinks to files not supported on windows" ) -def test_symlink_file_source_not_in_scm(inwd): +def test_symlink_file_source_not_in_scm(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "file1link").symlink_to("../file1") assert set(find_files("adir")) == _sep({"adir/filea"}) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") -def test_symlink_loop(inwd): +def test_symlink_loop(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "loop").symlink_to("../adir") inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea", "adir/loop"}) # -> ../adir @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") -def test_symlink_loop_outside_path(inwd): +def test_symlink_loop_outside_path(inwd: WorkDir) -> None: (inwd.cwd / "bdir" / "loop").symlink_to("../bdir") (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir") inwd.add_and_commit() @@ -110,7 +117,7 @@ def test_symlink_loop_outside_path(inwd): @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") -def test_symlink_dir_out_of_git(inwd): +def test_symlink_dir_out_of_git(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "outsidedirlink").symlink_to(os.path.join(__file__, "..")) inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea"}) @@ -119,19 +126,21 @@ def test_symlink_dir_out_of_git(inwd): @pytest.mark.skipif( sys.platform == "win32", reason="symlinks to files not supported on windows" ) -def test_symlink_file_out_of_git(inwd): +def test_symlink_file_out_of_git(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "outsidefilelink").symlink_to(__file__) inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea"}) @pytest.mark.parametrize("path_add", ["{cwd}", "{cwd}" + os.pathsep + "broken"]) -def test_ignore_root(inwd, monkeypatch, path_add): +def test_ignore_root( + inwd: WorkDir, monkeypatch: pytest.MonkeyPatch, path_add: str +) -> None: monkeypatch.setenv("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", path_add.format(cwd=inwd.cwd)) assert find_files() == [] -def test_empty_root(inwd): +def test_empty_root(inwd: WorkDir) -> None: subdir = inwd.cwd / "cdir" / "subdir" subdir.mkdir(parents=True) (subdir / "filec").touch() @@ -139,7 +148,7 @@ def test_empty_root(inwd): assert set(find_files("cdir")) == _sep({"cdir/subdir/filec"}) -def test_empty_subdir(inwd): +def test_empty_subdir(inwd: WorkDir) -> None: subdir = inwd.cwd / "adir" / "emptysubdir" / "subdir" subdir.mkdir(parents=True) (subdir / "xfile").touch() @@ -150,7 +159,7 @@ def test_empty_subdir(inwd): @pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows") -def test_double_include_through_symlink(inwd): +def test_double_include_through_symlink(inwd: WorkDir) -> None: (inwd.cwd / "data").mkdir() (inwd.cwd / "data" / "datafile").touch() (inwd.cwd / "adir" / "datalink").symlink_to("../data") @@ -169,7 +178,7 @@ def test_double_include_through_symlink(inwd): @pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows") -def test_symlink_not_in_scm_while_target_is(inwd): +def test_symlink_not_in_scm_while_target_is(inwd: WorkDir) -> None: (inwd.cwd / "data").mkdir() (inwd.cwd / "data" / "datafile").touch() inwd.add_and_commit() @@ -189,5 +198,5 @@ def test_symlink_not_in_scm_while_target_is(inwd): @pytest.mark.issue(587) @pytest.mark.skip_commit -def test_not_commited(inwd): +def test_not_commited(inwd: WorkDir) -> None: assert find_files() == [] diff --git a/testing/test_functions.py b/testing/test_functions.py index 33516321..fc62b5f5 100644 --- a/testing/test_functions.py +++ b/testing/test_functions.py @@ -1,6 +1,5 @@ from pathlib import Path -import pkg_resources import pytest from setuptools_scm import dump_version @@ -13,6 +12,8 @@ from setuptools_scm.version import meta from setuptools_scm.version import tag_to_version +c = Configuration() + @pytest.mark.parametrize( "tag, expected", @@ -20,16 +21,20 @@ ("1.1", "1.2"), ("1.2.dev", "1.2"), ("1.1a2", "1.1a3"), - ("23.24.post2+deadbeef", "23.24.post3"), + pytest.param( + "23.24.post2+deadbeef", + "23.24.post3", + marks=pytest.mark.filterwarnings( + "ignore:.*will be stripped of its suffix.*:UserWarning" + ), + ), ], ) -def test_next_tag(tag, expected): - version = pkg_resources.parse_version(tag) +def test_next_tag(tag: str, expected: str) -> None: + version = meta(tag, config=c) assert guess_next_version(version) == expected -c = Configuration() - VERSIONS = { "exact": meta("1.1", distance=None, dirty=False, config=c), "zerodistance": meta("1.1", distance=0, dirty=False, config=c), @@ -57,13 +62,13 @@ def test_next_tag(tag, expected): ("distancedirty", "post-release node-and-date", "1.1.post3+d20090213"), ], ) -def test_format_version(version, scheme, expected): - version = VERSIONS[version] +def test_format_version(version: str, scheme: str, expected: str) -> None: + scm_version = VERSIONS[version] vs, ls = scheme.split() - assert format_version(version, version_scheme=vs, local_scheme=ls) == expected + assert format_version(scm_version, version_scheme=vs, local_scheme=ls) == expected -def test_dump_version_doesnt_bail_on_value_error(tmp_path): +def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None: write_to = "VERSION" version = str(VERSIONS["exact"].tag) with pytest.raises(ValueError, match="^bad file format:"): diff --git a/testing/test_git.py b/testing/test_git.py index 44fbd7fc..27bccce3 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -4,6 +4,7 @@ from datetime import datetime from datetime import timezone from os.path import join as opj +from pathlib import Path from textwrap import dedent from typing import Dict from unittest.mock import Mock @@ -28,7 +29,7 @@ @pytest.fixture -def wd(wd, monkeypatch): +def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir: monkeypatch.delenv("HOME", raising=False) wd("git init") wd("git config user.email test@example.com") @@ -45,12 +46,14 @@ def wd(wd, monkeypatch): ("17.33.0-rc-17-g38c3047c0", "17.33.0-rc", 17, "g38c3047c0", False), ], ) -def test_parse_describe_output(given, tag, number, node, dirty): +def test_parse_describe_output( + given: str, tag: str, number: int, node: str, dirty: bool +) -> None: parsed = git._git_parse_describe(given) assert parsed == (tag, number, node, dirty) -def test_root_relative_to(tmpdir, wd, monkeypatch): +def test_root_relative_to(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") p = wd.cwd.joinpath("sub/package") p.mkdir(parents=True) @@ -64,7 +67,9 @@ def test_root_relative_to(tmpdir, wd, monkeypatch): assert res == "0.1.dev0" -def test_root_search_parent_directories(tmpdir, wd: WorkDir, monkeypatch): +def test_root_search_parent_directories( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") p = wd.cwd.joinpath("sub/package") p.mkdir(parents=True) @@ -77,7 +82,7 @@ def test_root_search_parent_directories(tmpdir, wd: WorkDir, monkeypatch): assert res == "0.1.dev0" -def test_git_gone(wd, monkeypatch): +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), git.DEFAULT_DESCRIBE) @@ -85,7 +90,7 @@ def test_git_gone(wd, monkeypatch): @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/298") @pytest.mark.issue(403) -def test_file_finder_no_history(wd, caplog): +def test_file_finder_no_history(wd: WorkDir, caplog: pytest.LogCaptureFixture) -> None: file_list = git_find_files(str(wd.cwd)) assert file_list == [] @@ -93,11 +98,11 @@ def test_file_finder_no_history(wd, caplog): @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/281") -def test_parse_call_order(wd): +def test_parse_call_order(wd: WorkDir) -> None: git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE) -def test_version_from_git(wd): +def test_version_from_git(wd: WorkDir) -> None: assert wd.version == "0.1.dev0" parsed = git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE) @@ -171,8 +176,8 @@ def __repr__(self): [pytest.param(text, id=key) for key, text in setup_py_with_normalize.items()], ) def test_git_version_unnormalized_setuptools( - setup_py_txt: str, wd: WorkDir, monkeypatch -): + setup_py_txt: str, wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: """ Test that when integrating with setuptools without normalization, the version is not normalized in write_to files, @@ -195,14 +200,14 @@ def test_git_version_unnormalized_setuptools( @pytest.mark.issue(179) -def test_unicode_version_scheme(wd): +def test_unicode_version_scheme(wd: WorkDir) -> None: scheme = b"guess-next-dev".decode("ascii") assert wd.get_version(version_scheme=scheme) @pytest.mark.issue(108) @pytest.mark.issue(109) -def test_git_worktree(wd): +def test_git_worktree(wd: WorkDir) -> None: wd.write("test.txt", "test2") # untracked files dont change the state assert wd.version == "0.1.dev0" @@ -212,7 +217,9 @@ def test_git_worktree(wd): @pytest.mark.issue(86) @pytest.mark.parametrize("today", [False, True]) -def test_git_dirty_notag(today, wd, monkeypatch): +def test_git_dirty_notag( + today: bool, wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: if today: monkeypatch.delenv("SOURCE_DATE_EPOCH", raising=False) wd.commit_testfile() @@ -230,7 +237,7 @@ def test_git_dirty_notag(today, wd, monkeypatch): @pytest.mark.issue(193) @pytest.mark.xfail(reason="sometimes relative path results") -def test_git_worktree_support(wd, tmp_path): +def test_git_worktree_support(wd: WorkDir, tmp_path: Path) -> None: wd.commit_testfile() worktree = tmp_path / "work_tree" wd("git worktree add -b work-tree %s" % worktree) @@ -241,36 +248,38 @@ def test_git_worktree_support(wd, tmp_path): @pytest.fixture -def shallow_wd(wd, tmpdir): +def shallow_wd(wd: WorkDir, tmp_path: Path) -> Path: wd.commit_testfile() wd.commit_testfile() wd.commit_testfile() - target = tmpdir.join("wd_shallow") + target = tmp_path / "wd_shallow" do(["git", "clone", "file://%s" % wd.cwd, str(target), "--depth=1"]) return target -def test_git_parse_shallow_warns(shallow_wd, recwarn): +def test_git_parse_shallow_warns( + shallow_wd: Path, recwarn: pytest.WarningsRecorder +) -> None: git.parse(str(shallow_wd)) msg = recwarn.pop() assert "is shallow and may cause errors" in str(msg.message) -def test_git_parse_shallow_fail(shallow_wd): - with pytest.raises(ValueError) as einfo: +def test_git_parse_shallow_fail(shallow_wd: Path) -> None: + with pytest.raises(ValueError, match="git fetch"): git.parse(str(shallow_wd), pre_parse=git.fail_on_shallow) - assert "git fetch" in str(einfo.value) - -def test_git_shallow_autocorrect(shallow_wd, recwarn): +def test_git_shallow_autocorrect( + shallow_wd: Path, recwarn: pytest.WarningsRecorder +) -> None: git.parse(str(shallow_wd), pre_parse=git.fetch_on_shallow) msg = recwarn.pop() assert "git fetch was used to rectify" in str(msg.message) git.parse(str(shallow_wd), pre_parse=git.fail_on_shallow) -def test_find_files_stop_at_root_git(wd): +def test_find_files_stop_at_root_git(wd: WorkDir) -> None: wd.commit_testfile() project = wd.cwd / "project" project.mkdir() @@ -279,18 +288,20 @@ def test_find_files_stop_at_root_git(wd): @pytest.mark.issue(128) -def test_parse_no_worktree(tmpdir): - ret = git.parse(str(tmpdir)) +def test_parse_no_worktree(tmp_path: Path) -> None: + ret = git.parse(str(tmp_path)) assert ret is None -def test_alphanumeric_tags_match(wd): +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") -def test_git_archive_export_ignore(wd, monkeypatch): +def test_git_archive_export_ignore( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: wd.write("test1.txt", "test") wd.write("test2.txt", "test") wd.write( @@ -306,7 +317,7 @@ def test_git_archive_export_ignore(wd, monkeypatch): @pytest.mark.issue(228) -def test_git_archive_subdirectory(wd, monkeypatch): +def test_git_archive_subdirectory(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: os.mkdir(wd.cwd / "foobar") wd.write("foobar/test1.txt", "test") wd("git add foobar") @@ -316,7 +327,9 @@ def test_git_archive_subdirectory(wd, monkeypatch): @pytest.mark.issue(251) -def test_git_archive_run_from_subdirectory(wd, monkeypatch): +def test_git_archive_run_from_subdirectory( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: os.mkdir(wd.cwd / "foobar") wd.write("foobar/test1.txt", "test") wd("git add foobar") @@ -325,7 +338,7 @@ def test_git_archive_run_from_subdirectory(wd, monkeypatch): assert integration.find_files(".") == [opj(".", "test1.txt")] -def test_git_feature_branch_increments_major(wd): +def test_git_feature_branch_increments_major(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag 1.0.0") wd.commit_testfile() @@ -336,7 +349,7 @@ def test_git_feature_branch_increments_major(wd): @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/303") -def test_not_matching_tags(wd): +def test_not_matching_tags(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag apache-arrow-0.11.1") wd.commit_testfile() @@ -350,21 +363,21 @@ def test_not_matching_tags(wd): @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/411") @pytest.mark.xfail(reason="https://github.com/pypa/setuptools_scm/issues/449") -def test_non_dotted_version(wd): +def test_non_dotted_version(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag apache-arrow-1") wd.commit_testfile() assert wd.get_version().startswith("2") -def test_non_dotted_version_with_updated_regex(wd): +def test_non_dotted_version_with_updated_regex(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag apache-arrow-1") wd.commit_testfile() assert wd.get_version(tag_regex=r"^apache-arrow-([\.0-9]+)$").startswith("2") -def test_non_dotted_tag_no_version_match(wd): +def test_non_dotted_tag_no_version_match(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag apache-arrow-0.11.1") wd.commit_testfile() @@ -374,7 +387,7 @@ def test_non_dotted_tag_no_version_match(wd): @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/381") -def test_gitdir(monkeypatch, wd): +def test_gitdir(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: """ """ wd.commit_testfile() normal = wd.version @@ -383,13 +396,14 @@ def test_gitdir(monkeypatch, wd): assert wd.version == normal -def test_git_getdate(wd): +def test_git_getdate(wd: WorkDir) -> None: # TODO: case coverage for git wd parse today = date.today() - def parse_date(): + def parse_date() -> date: parsed = git.parse(os.fspath(wd.cwd)) assert parsed is not None + assert parsed.node_date is not None return parsed.node_date git_wd = git.GitWorkdir(os.fspath(wd.cwd)) @@ -401,7 +415,7 @@ def parse_date(): assert parse_date() == today -def test_git_getdate_badgit(wd): +def test_git_getdate_badgit(wd: WorkDir) -> None: wd.commit_testfile() git_wd = git.GitWorkdir(os.fspath(wd.cwd)) with patch.object(git_wd, "do_ex", Mock(return_value=("%cI", "", 0))): @@ -409,7 +423,7 @@ def test_git_getdate_badgit(wd): @pytest.fixture -def signed_commit_wd(monkeypatch, wd): +def signed_commit_wd(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> WorkDir: if not has_command("gpg", args=["--version"], warn=False): pytest.skip("gpg executable not found") @@ -434,7 +448,7 @@ def signed_commit_wd(monkeypatch, wd): @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/548") -def test_git_getdate_signed_commit(signed_commit_wd): +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)) @@ -461,7 +475,7 @@ def test_git_getdate_signed_commit(signed_commit_wd): ], ) @pytest.mark.filterwarnings("ignore:git archive did not support describe output") -def test_git_archival_to_version(expected, from_data): +def test_git_archival_to_version(expected: str, from_data: Dict[str, str]) -> None: config = Configuration() version = archival_to_version(from_data, config=config) assert ( diff --git a/testing/test_hg_git.py b/testing/test_hg_git.py index 3a60d96d..1271b721 100644 --- a/testing/test_hg_git.py +++ b/testing/test_hg_git.py @@ -1,11 +1,14 @@ +from typing import Tuple + import pytest from setuptools_scm.utils import do_ex from setuptools_scm.utils import has_command +from testing.wd_wrapper import WorkDir @pytest.fixture(scope="module", autouse=True) -def _check_hg_git(): +def _check_hg_git() -> None: if not has_command("hg", warn=False): pytest.skip("hg executable not found") @@ -20,7 +23,7 @@ def _check_hg_git(): pytest.skip("hg-git not installed") -def test_base(repositories_hg_git): +def test_base(repositories_hg_git: Tuple[WorkDir, WorkDir]) -> None: wd, wd_git = repositories_hg_git assert wd_git.version == "0.1.dev0" diff --git a/testing/test_integration.py b/testing/test_integration.py index 29c4cdf6..9759fa83 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1,9 +1,11 @@ import os import sys import textwrap +from pathlib import Path import pytest +from .wd_wrapper import WorkDir from setuptools_scm import PRETEND_KEY from setuptools_scm import PRETEND_KEY_NAMED from setuptools_scm.integration import _warn_on_old_setuptools @@ -11,7 +13,7 @@ @pytest.fixture -def wd(wd): +def wd(wd: WorkDir) -> WorkDir: wd("git init") wd("git config user.email test@example.com") wd('git config user.name "a test"') @@ -20,11 +22,12 @@ def wd(wd): return wd -def test_pyproject_support(tmpdir, monkeypatch): +def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: pytest.importorskip("tomli") monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") - pkg = tmpdir.ensure("package", dir=42) - pkg.join("pyproject.toml").write_text( + pkg = tmp_path / "package" + pkg.mkdir() + pkg.joinpath("pyproject.toml").write_text( textwrap.dedent( """ [tool.setuptools_scm] @@ -38,8 +41,8 @@ def test_pyproject_support(tmpdir, monkeypatch): ), encoding="utf-8", ) - pkg.join("setup.py").write("__import__('setuptools').setup()") - res = do((sys.executable, "setup.py", "--version"), pkg) + pkg.joinpath("setup.py").write_text("__import__('setuptools').setup()") + res = do([sys.executable, "setup.py", "--version"], pkg) assert res == "12.34" @@ -78,16 +81,16 @@ def test_pyproject_support(tmpdir, monkeypatch): @with_metadata_in -def test_pyproject_support_with_git(wd, metadata_in): +def test_pyproject_support_with_git(wd: WorkDir, metadata_in: str) -> None: pytest.importorskip("tomli") wd.write("pyproject.toml", PYPROJECT_FILES[metadata_in]) 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")) + res = wd([sys.executable, "setup.py", "--version"]) assert res.endswith("0.1.dev0") -def test_pretend_version(monkeypatch, wd): +def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY, "1.0.0") assert wd.get_version() == "1.0.0" @@ -95,36 +98,42 @@ def test_pretend_version(monkeypatch, wd): @with_metadata_in -def test_pretend_version_named_pyproject_integration(monkeypatch, wd, metadata_in): +def test_pretend_version_named_pyproject_integration( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir, metadata_in: str +) -> None: test_pyproject_support_with_git(wd, metadata_in) monkeypatch.setenv( PRETEND_KEY_NAMED.format(name="setuptools_scm_example".upper()), "3.2.1" ) - res = wd((sys.executable, "setup.py", "--version")) + res = wd([sys.executable, "setup.py", "--version"]) assert res.endswith("3.2.1") -def test_pretend_version_named(tmpdir, monkeypatch, wd): +def test_pretend_version_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test2".upper()), "2.0.0") assert wd.get_version(dist_name="test") == "1.0.0" assert wd.get_version(dist_name="test2") == "2.0.0" -def test_pretend_version_name_takes_precedence(tmpdir, monkeypatch, wd): +def test_pretend_version_name_takes_precedence( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir +) -> None: monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") monkeypatch.setenv(PRETEND_KEY, "2.0.0") assert wd.get_version(dist_name="test") == "1.0.0" -def test_pretend_version_accepts_bad_string(monkeypatch, wd): +def test_pretend_version_accepts_bad_string( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir +) -> None: monkeypatch.setenv(PRETEND_KEY, "dummy") wd.write("setup.py", SETUP_PY_PLAIN) assert wd.get_version(write_to="test.py") == "dummy" assert wd("python setup.py --version") == "0.0.0" -def test_own_setup_fails_on_old_python(monkeypatch): +def test_own_setup_fails_on_old_python(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr("sys.version_info", (3, 5)) monkeypatch.syspath_prepend(os.path.dirname(os.path.dirname(__file__))) @@ -137,18 +146,18 @@ def test_own_setup_fails_on_old_python(monkeypatch): setup.scm_version() -def testwarn_on_broken_setuptools(): +def testwarn_on_broken_setuptools() -> None: _warn_on_old_setuptools("45") with pytest.warns(RuntimeWarning, match="ERROR: setuptools==44"): _warn_on_old_setuptools("44") @pytest.mark.issue(611) -def test_distribution_procides_extras(): +def test_distribution_procides_extras() -> None: try: - from importlib.metadata import distribution + from importlib.metadata import distribution # type: ignore except ImportError: - from importlib_metadata import distribution + from importlib_metadata import distribution # type: ignore dist = distribution("setuptools_scm") assert sorted(dist.metadata.get_all("Provides-Extra")) == ["test", "toml"] diff --git a/testing/test_main.py b/testing/test_main.py index ea1373f7..41588bb7 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -4,18 +4,21 @@ import pytest +from .wd_wrapper import WorkDir -def test_main(): + +def test_main() -> None: mainfile = os.path.join( os.path.dirname(__file__), "..", "src", "setuptools_scm", "__main__.py" ) + ns = {"__package__": "setuptools_scm"} with open(mainfile) as f: code = compile(f.read(), "__main__.py", "exec") - exec(code) + exec(code, ns) @pytest.fixture -def repo(wd): +def repo(wd: WorkDir) -> WorkDir: wd("git init") wd("git config user.email user@host") wd("git config user.name user") @@ -32,7 +35,7 @@ def repo(wd): return wd -def test_repo_with_config(repo): +def test_repo_with_config(repo: WorkDir) -> None: pyproject = """\ [tool.setuptools_scm] version_scheme = "no-guess-dev" @@ -42,21 +45,21 @@ def test_repo_with_config(repo): """ repo.write("pyproject.toml", textwrap.dedent(pyproject)) repo.add_and_commit() - res = repo((sys.executable, "-m", "setuptools_scm")) + res = repo([sys.executable, "-m", "setuptools_scm"]) assert res.startswith("0.1.0.post1.dev2") -def test_repo_without_config(repo): - res = repo((sys.executable, "-m", "setuptools_scm")) +def test_repo_without_config(repo: WorkDir) -> None: + res = repo([sys.executable, "-m", "setuptools_scm"]) assert res.startswith("0.1.1.dev1") -def test_repo_with_pyproject_missing_setuptools_scm(repo): +def test_repo_with_pyproject_missing_setuptools_scm(repo: WorkDir) -> None: pyproject = """\ [project] name = "example" """ repo.write("pyproject.toml", textwrap.dedent(pyproject)) repo.add_and_commit() - res = repo((sys.executable, "-m", "setuptools_scm")) + res = repo([sys.executable, "-m", "setuptools_scm"]) assert res.startswith("0.1.1.dev2") diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index b54bb6f3..0b77e377 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -1,3 +1,7 @@ +import os +from pathlib import Path +from typing import Dict + import pytest from setuptools_scm import format_version @@ -6,6 +10,7 @@ from setuptools_scm.hg import archival_to_version from setuptools_scm.hg import parse from setuptools_scm.utils import has_command +from testing.wd_wrapper import WorkDir pytestmark = pytest.mark.skipif( @@ -14,7 +19,7 @@ @pytest.fixture -def wd(wd): +def wd(wd: WorkDir) -> WorkDir: wd("hg init") wd.add_command = "hg add ." wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' @@ -35,7 +40,7 @@ def wd(wd): @pytest.mark.parametrize("expected,data", sorted(archival_mapping.items())) -def test_archival_to_version(expected, data): +def test_archival_to_version(expected: str, data: Dict[str, str]) -> None: config = Configuration() version = archival_to_version(data, config=config) assert ( @@ -46,13 +51,15 @@ def test_archival_to_version(expected, data): ) -def test_hg_gone(wd, monkeypatch): +def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) with pytest.raises(EnvironmentError, match="'hg' was not found"): parse(str(wd.cwd)) -def test_find_files_stop_at_root_hg(wd, monkeypatch): +def test_find_files_stop_at_root_hg( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: wd.commit_testfile() project = wd.cwd / "project" project.mkdir() @@ -66,11 +73,11 @@ def test_find_files_stop_at_root_hg(wd, monkeypatch): # XXX: better tests for tag prefixes -def test_version_from_hg_id(wd): +def test_version_from_hg_id(wd: WorkDir) -> None: assert wd.version == "0.0" wd.commit_testfile() - assert wd.version.startswith("0.1.dev2+") + assert wd.version.startswith("0.1.dev1+") # tagging commit is considered the tag wd('hg tag v0.1 -u test -d "0 0"') @@ -94,7 +101,7 @@ def test_version_from_hg_id(wd): assert wd.version == "0.3" -def test_version_from_archival(wd): +def test_version_from_archival(wd: WorkDir) -> None: # entrypoints are unordered, # cleaning the wd ensure this test won't break randomly wd.cwd.joinpath(".hg").rename(wd.cwd / ".nothg") @@ -110,7 +117,7 @@ def test_version_from_archival(wd): @pytest.mark.issue("#72") -def test_version_in_merge(wd): +def test_version_in_merge(wd: WorkDir) -> None: wd.commit_testfile() wd.commit_testfile() wd("hg up 0") @@ -120,13 +127,13 @@ def test_version_in_merge(wd): @pytest.mark.issue(128) -def test_parse_no_worktree(tmpdir): - ret = parse(str(tmpdir)) +def test_parse_no_worktree(tmp_path: Path) -> None: + ret = parse(os.fspath(tmp_path)) assert ret is None @pytest.fixture -def version_1_0(wd): +def version_1_0(wd: WorkDir) -> WorkDir: wd("hg branch default") wd.commit_testfile() wd('hg tag 1.0.0 -u test -d "0 0"') @@ -134,7 +141,8 @@ def version_1_0(wd): @pytest.fixture -def pre_merge_commit_after_tag(wd, version_1_0): +def pre_merge_commit_after_tag(version_1_0: WorkDir) -> WorkDir: + wd = version_1_0 wd("hg branch testbranch") wd.write("branchfile", "branchtext") wd(wd.add_command) @@ -145,19 +153,19 @@ def pre_merge_commit_after_tag(wd, version_1_0): @pytest.mark.usefixtures("pre_merge_commit_after_tag") -def test_version_bump_before_merge_commit(wd): +def test_version_bump_before_merge_commit(wd: WorkDir) -> None: assert wd.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): +def test_version_bump_from_merge_commit(wd: WorkDir) -> None: wd.commit() assert wd.version.startswith("1.0.1.dev3+") # issue 219 @pytest.mark.usefixtures("version_1_0") -def test_version_bump_from_commit_including_hgtag_mods(wd): +def test_version_bump_from_commit_including_hgtag_mods(wd: WorkDir) -> None: """Test the case where a commit includes changes to .hgtags and other files""" with wd.cwd.joinpath(".hgtags").open("ab") as tagfile: tagfile.write(b"0 0\n") @@ -170,7 +178,7 @@ def test_version_bump_from_commit_including_hgtag_mods(wd): @pytest.mark.issue(229) @pytest.mark.usefixtures("version_1_0") -def test_latest_tag_detection(wd): +def test_latest_tag_detection(wd: WorkDir) -> None: """Tests that tags not containing a "." are ignored, the same as for git. Note that will be superseded by the fix for pypa/setuptools_scm/issues/235 """ @@ -179,7 +187,7 @@ def test_latest_tag_detection(wd): @pytest.mark.usefixtures("version_1_0") -def test_feature_branch_increments_major(wd) -> None: +def test_feature_branch_increments_major(wd: WorkDir) -> None: wd.commit_testfile() assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.0.1") diff --git a/testing/test_regressions.py b/testing/test_regressions.py index 1b88457d..c7168579 100644 --- a/testing/test_regressions.py +++ b/testing/test_regressions.py @@ -1,5 +1,7 @@ +import os import subprocess import sys +from pathlib import Path import pytest @@ -9,63 +11,72 @@ from setuptools_scm.utils import do_ex -def test_pkginfo_noscmroot(tmpdir, monkeypatch): +def test_pkginfo_noscmroot(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """if we are indeed a sdist, the root does not apply""" monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") # we should get the version from pkg-info if git is broken - p = tmpdir.ensure("sub/package", dir=1) - tmpdir.mkdir(".git") - p.join("setup.py").write( + 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": ".."})' ) - _, stderr, ret = do_ex((sys.executable, "setup.py", "--version"), p) + _, stderr, ret = do_ex([sys.executable, "setup.py", "--version"], p) assert "setuptools-scm was unable to detect version for" in stderr assert ret == 1 - p.join("PKG-INFO").write("Version: 1.0") - res = do((sys.executable, "setup.py", "--version"), p) + p.joinpath("PKG-INFO").write_text("Version: 1.0") + res = do([sys.executable, "setup.py", "--version"], p) assert res == "1.0" try: - do("git init", p.dirpath()) + do("git init", p.parent) except OSError: pass else: - res = do((sys.executable, "setup.py", "--version"), p) + res = do([sys.executable, "setup.py", "--version"], p) assert res == "0.1.dev0" -def test_pip_egg_info(tmpdir, monkeypatch): +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 = tmpdir.ensure("sub/package", dir=1) - tmpdir.mkdir(".git") - p.join("setup.py").write( + 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=p.strpath, fallback_root=p.strpath) + 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) - p.ensure("pip-egg-info/random.egg-info/PKG-INFO").write("Version: 1.0") - assert get_version(root=p.strpath, fallback_root=p.strpath) == "1.0" + 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" @pytest.mark.issue(164) -def test_pip_download(tmpdir, monkeypatch): - monkeypatch.chdir(tmpdir) +def test_pip_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.chdir(tmp_path) subprocess.check_call([sys.executable, "-m", "pip", "download", "lz4==0.9.0"]) -def test_use_scm_version_callable(tmpdir, monkeypatch): +def test_use_scm_version_callable( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: """use of callable as use_scm_version argument""" monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") - p = tmpdir.ensure("sub/package", dir=1) - p.join("setup.py").write( + p = tmp_path / "sub" / "package" + p.mkdir(parents=True) + p.joinpath("setup.py").write_text( """from setuptools import setup def vcfg(): from setuptools_scm.version import guess_next_dev_version @@ -75,17 +86,17 @@ def vs(v): setup(use_scm_version=vcfg) """ ) - p.join("PKG-INFO").write("Version: 1.0") + p.joinpath("PKG-INFO").write_text("Version: 1.0") - res = do((sys.executable, "setup.py", "--version"), p) + res = do([sys.executable, "setup.py", "--version"], p) assert res == "1.0" @pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows") -def test_case_mismatch_on_windows_git(tmpdir): +def test_case_mismatch_on_windows_git(tmp_path: Path) -> None: """Case insensitive path checks on Windows""" - p = tmpdir.ensure("CapitalizedDir", dir=1) - - do("git init", p) - res = parse(str(p).lower()) + camel_case_path = tmp_path / "CapitalizedDir" + camel_case_path.mkdir() + do("git init", camel_case_path) + res = parse(str(camel_case_path).lower()) assert res is not None diff --git a/testing/test_setuptools_support.py b/testing/test_setuptools_support.py index bd8cad10..f6954520 100644 --- a/testing/test_setuptools_support.py +++ b/testing/test_setuptools_support.py @@ -5,18 +5,22 @@ import pathlib import subprocess import sys +from typing import Any +from typing import Callable +from typing import List +import _pytest.config import pytest -def cli_run(*k, **kw): +def cli_run(*k: Any, **kw: Any) -> None: """this defers the virtualenv import it helps to avoid warnings from the furthermore imported setuptools """ global cli_run - from virtualenv.run import cli_run + from virtualenv.run import cli_run # type: ignore - return cli_run(*k, **kw) + cli_run(*k, **kw) pytestmark = pytest.mark.filterwarnings( @@ -28,11 +32,13 @@ def cli_run(*k, **kw): class Venv: + location: pathlib.Path + def __init__(self, location: pathlib.Path): self.location = location @property - def python(self): + def python(self) -> pathlib.Path: return self.location / "bin/python" @@ -40,10 +46,12 @@ class VenvMaker: def __init__(self, base: pathlib.Path): self.base = base - def __repr__(self): + def __repr__(self) -> str: return f"" - def get_venv(self, python, pip, setuptools, prefix="scm"): + def get_venv( + self, python: str, pip: str, setuptools: str, prefix: str = "scm" + ) -> Venv: name = f"{prefix}-py={python}-pip={pip}-setuptools={setuptools}" path = self.base / name if not path.is_dir(): @@ -70,13 +78,13 @@ def get_venv(self, python, pip, setuptools, prefix="scm"): @pytest.fixture -def venv_maker(pytestconfig): +def venv_maker(pytestconfig: _pytest.config.Config) -> VenvMaker: if not pytestconfig.getoption("--test-legacy"): pytest.skip( "testing on legacy setuptools disabled, pass --test-legacy to run them" ) - dir = pytestconfig.cache.makedir("setuptools_scm_venvs") - path = pathlib.Path(str(dir)) + assert pytestconfig.cache is not None + path = pytestconfig.cache.mkdir("setuptools_scm_venvs") return VenvMaker(path) @@ -91,7 +99,7 @@ def venv_maker(pytestconfig): """ -def check(venv, expected_version, **env): +def check(venv: Venv, expected_version: str, **env: str) -> None: subprocess.check_call( [venv.python, "-c", SCRIPT, expected_version], @@ -102,7 +110,7 @@ def check(venv, expected_version, **env): @pytest.mark.skipif( sys.version_info[:2] >= (3, 10), reason="old setuptools won't work on python 3.10" ) -def test_distlib_setuptools_works(venv_maker): +def test_distlib_setuptools_works(venv_maker: VenvMaker) -> None: venv = venv_maker.get_venv(setuptools="45.0.0", pip="9.0", python="3.6") subprocess.run([venv.python, "-m", "pip", "install", "-e", str(ROOT)]) @@ -133,14 +141,14 @@ def test_distlib_setuptools_works(venv_maker): """ -def prepare_expecting_pyproject_support(pkg: pathlib.Path): +def prepare_expecting_pyproject_support(pkg: pathlib.Path) -> None: pkg.mkdir() pkg.joinpath("setup.py").write_text(SETUP_PY_NAME) pkg.joinpath("pyproject.toml").write_text(PYPROJECT_TOML_WITH_KEY) pkg.joinpath("PKG-INFO").write_text("Version: 1.0.0") -def prepare_setup_py_config(pkg: pathlib.Path): +def prepare_setup_py_config(pkg: pathlib.Path) -> None: pkg.mkdir() pkg.joinpath("setup.py").write_text(SETUP_PY_KEYWORD) pkg.joinpath("setup.cfg").write_text(SETUP_CFG_NAME) @@ -163,15 +171,18 @@ def prepare_setup_py_config(pkg: pathlib.Path): ], ) def test_on_old_setuptools( - venv_maker, tmp_path, setuptools, project_create, monkeypatch -): + venv_maker: VenvMaker, + tmp_path: pathlib.Path, + setuptools: str, + project_create: Callable[[pathlib.Path], None], +) -> None: pkg = tmp_path.joinpath("pkg") project_create(pkg) venv = venv_maker.get_venv(setuptools=setuptools, pip="9.0", python="3.6") # monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG", raising=False) - def run_and_output(cmd): + def run_and_output(cmd: "List[str | pathlib.Path]") -> bytes: res = subprocess.run(cmd, cwd=str(pkg), stdout=subprocess.PIPE) if not res.returncode: return res.stdout.strip() diff --git a/testing/test_version.py b/testing/test_version.py index 5a413c9b..aecceb64 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -1,5 +1,6 @@ from datetime import date from datetime import timedelta +from typing import Any import pytest @@ -10,6 +11,7 @@ from setuptools_scm.version import meta from setuptools_scm.version import no_guess_dev_version from setuptools_scm.version import release_branch_semver_version +from setuptools_scm.version import ScmVersion from setuptools_scm.version import simplified_semver_version from setuptools_scm.version import tags_to_versions @@ -49,16 +51,16 @@ ), ], ) -def test_next_semver(version, expected_next): +def test_next_semver(version: ScmVersion, expected_next: str) -> None: computed = simplified_semver_version(version) assert computed == expected_next -def test_next_semver_bad_tag(): +def test_next_semver_bad_tag() -> None: version = meta("1.0.0-foo", preformatted=True, config=c) with pytest.raises( - ValueError, match="1.0.0-foo can't be parsed as numeric version" + ValueError, match=r"1\.0\.0-foo.* can't be parsed as numeric version" ): simplified_semver_version(version) @@ -99,12 +101,12 @@ def test_next_semver_bad_tag(): ), ], ) -def test_next_release_branch_semver(version, expected_next): +def test_next_release_branch_semver(version: ScmVersion, expected_next: str) -> None: computed = release_branch_semver_version(version) assert computed == expected_next -def m(tag, **kw): +def m(tag: str, **kw: Any) -> ScmVersion: return meta(tag, **kw, config=c) @@ -131,7 +133,7 @@ def m(tag, **kw): ), ], ) -def test_no_guess_version(version, expected_next): +def test_no_guess_version(version: ScmVersion, expected_next: str) -> None: computed = no_guess_dev_version(version) assert computed == expected_next @@ -143,18 +145,18 @@ def test_no_guess_version(version, expected_next): ("1.0.post1", "already is a post release"), ], ) -def test_no_guess_version_bad(version, match): +def test_no_guess_version_bad(version: str, match: str) -> None: with pytest.raises(ValueError, match=match): no_guess_dev_version(m(version, distance=1)) -def test_bump_dev_version_zero(): - assert guess_next_version("1.0.dev0") == "1.0" +def test_bump_dev_version_zero() -> None: + assert guess_next_version(m("1.0.dev0")) == "1.0" -def test_bump_dev_version_nonzero_raises(): +def test_bump_dev_version_nonzero_raises() -> None: with pytest.raises(ValueError) as excinfo: - guess_next_version("1.0.dev1") + guess_next_version(m("1.0.dev1")) assert str(excinfo.value) == ( "choosing custom numbers for the `.devX` distance " @@ -167,12 +169,12 @@ def test_bump_dev_version_nonzero_raises(): @pytest.mark.parametrize( "tag, expected", [ - pytest.param("v1.0.0", "1.0.0"), - pytest.param("v1.0.0-rc.1", "1.0.0rc1"), - pytest.param("v1.0.0-rc.1+-25259o4382757gjurh54", "1.0.0rc1"), + ("v1.0.0", "1.0.0"), + ("v1.0.0-rc.1", "1.0.0rc1"), + ("v1.0.0-rc.1+-25259o4382757gjurh54", "1.0.0rc1"), ], ) -def test_tag_regex1(tag, expected): +def test_tag_regex1(tag: str, expected: str) -> None: if "+" in tag: # pytest bug wrt cardinality with pytest.warns(UserWarning): @@ -184,23 +186,33 @@ def test_tag_regex1(tag, expected): @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/286") -def test_tags_to_versions(): +def test_tags_to_versions() -> None: versions = tags_to_versions(["1.0", "2.0", "3.0"], config=c) assert isinstance(versions, list) # enable subscription @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471") -def test_version_bump_bad(): +def test_version_bump_bad() -> None: + class YikesVersion: + val: str + + def __init__(self, val: str): + self.val = val + + def __str__(self) -> str: + return self.val + + config = Configuration(version_cls=YikesVersion) with pytest.raises( ValueError, match=".*does not end with a number to bump, " "please correct or use a custom version scheme", ): - guess_next_version(tag_version="2.0.0-alpha.5-PMC") + guess_next_version(tag_version=meta("2.0.0-alpha.5-PMC", config=config)) -def test_format_version_schemes(): +def test_format_version_schemes() -> None: version = meta("1.0", config=c) format_version( version, @@ -209,7 +221,11 @@ def test_format_version_schemes(): ) -def date_to_str(date_=None, days_offset=0, fmt="{dt:%y}.{dt.month}.{dt.day}"): +def date_to_str( + date_: "date | None" = None, + days_offset: int = 0, + fmt: str = "{dt:%y}.{dt.month}.{dt.day}", +) -> str: date_ = date_ or date.today() date_ = date_ - timedelta(days=days_offset) return fmt.format(dt=date_) @@ -290,7 +306,7 @@ def date_to_str(date_=None, days_offset=0, fmt="{dt:%y}.{dt.month}.{dt.day}"): ), ], ) -def test_calver_by_date(version, expected_next): +def test_calver_by_date(version: ScmVersion, expected_next: str) -> None: computed = calver_by_date(version) assert computed == expected_next @@ -307,27 +323,30 @@ def test_calver_by_date(version, expected_next): ), ], ) -def test_calver_by_date_semver(version, expected_next): +def test_calver_by_date_semver(version: ScmVersion, expected_next: str) -> None: computed = calver_by_date(version) assert computed == expected_next -def test_calver_by_date_future_warning(): +def test_calver_by_date_future_warning() -> None: with pytest.warns(UserWarning, match="your previous tag*"): calver_by_date(meta(date_to_str(days_offset=-2), config=c, distance=2)) -def test_custom_version_cls(): +def test_custom_version_cls() -> None: """Test that we can pass our own version class instead of pkg_resources""" class MyVersion: def __init__(self, tag_str: str): self.tag = tag_str - def __repr__(self): + def __str__(self) -> str: return "Custom %s" % self.tag + def __repr__(self) -> str: + return "MyVersion" % self.tag + scm_version = meta("1.0.0-foo", config=Configuration(version_cls=MyVersion)) assert isinstance(scm_version.tag, MyVersion) - assert repr(scm_version.tag) == "Custom 1.0.0-foo" + assert str(scm_version.tag) == "Custom 1.0.0-foo" diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py index e7c92d0f..3fbdd1d2 100644 --- a/testing/wd_wrapper.py +++ b/testing/wd_wrapper.py @@ -1,5 +1,6 @@ import itertools from pathlib import Path +from typing import Any from typing import List @@ -10,14 +11,14 @@ class WorkDir: signed_commit_command: str add_command: str - def __repr__(self): + def __repr__(self) -> str: return f"" def __init__(self, cwd: Path) -> None: self.cwd = cwd self.__counter = itertools.count() - def __call__(self, cmd: "List[str] | str", **kw): + def __call__(self, cmd: "List[str] | str", **kw: object) -> str: if kw: assert isinstance(cmd, str), "formatting the command requires text input" cmd = cmd.format(**kw) @@ -42,9 +43,11 @@ def _reason(self, given_reason: "str | None") -> str: else: return given_reason - def add_and_commit(self, reason: "str | None" = None, **kwargs): + def add_and_commit( + self, reason: "str | None" = None, signed: bool = False, **kwargs: object + ) -> None: self(self.add_command) - self.commit(reason, **kwargs) + self.commit(reason=reason, signed=signed, **kwargs) def commit(self, reason: "str | None" = None, signed: bool = False) -> None: reason = self._reason(reason) @@ -53,13 +56,15 @@ def commit(self, reason: "str | None" = None, signed: bool = False) -> None: reason=reason, ) - def commit_testfile(self, reason: "str | None" = None, signed: bool = False): + 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(self.add_command) self.commit(reason=reason, signed=signed) - def get_version(self, **kw): + def get_version(self, **kw: Any) -> str: __tracebackhide__ = True from setuptools_scm import get_version @@ -68,6 +73,6 @@ def get_version(self, **kw): return version @property - def version(self): + def version(self) -> str: __tracebackhide__ = True return self.get_version() From d65a0377a71b2915107d4e816c0f5e1724d42f82 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 7 May 2022 21:37:00 +0200 Subject: [PATCH 08/18] Bump minimal python to 3.7 --- .pre-commit-config.yaml | 4 +- CHANGELOG.rst | 7 +++ mypy.ini | 4 +- setup.cfg | 3 +- setup.py | 21 ++------- src/setuptools_scm/__init__.py | 38 ++++++++-------- src/setuptools_scm/__main__.py | 2 + src/setuptools_scm/_cli.py | 2 + src/setuptools_scm/_entrypoints.py | 15 ++++--- src/setuptools_scm/_overrides.py | 7 +-- src/setuptools_scm/_types.py | 2 + src/setuptools_scm/_version_cls.py | 8 ++-- src/setuptools_scm/config.py | 63 ++++++++++++++------------- src/setuptools_scm/discover.py | 2 + src/setuptools_scm/file_finder.py | 2 +- src/setuptools_scm/file_finder_git.py | 14 +++--- src/setuptools_scm/file_finder_hg.py | 27 +++++------- src/setuptools_scm/git.py | 45 +++++++++---------- src/setuptools_scm/hacks.py | 13 +++--- src/setuptools_scm/hg.py | 20 ++++----- src/setuptools_scm/hg_git.py | 25 +++++------ src/setuptools_scm/integration.py | 8 ++-- src/setuptools_scm/scm_workdir.py | 2 + src/setuptools_scm/utils.py | 20 ++++----- src/setuptools_scm/version.py | 2 +- testing/check_self_install.py | 2 + testing/conftest.py | 8 ++-- testing/test_basic_api.py | 2 + testing/test_config.py | 2 + testing/test_file_finder.py | 5 ++- testing/test_functions.py | 2 + testing/test_git.py | 7 +-- testing/test_hg_git.py | 4 +- testing/test_integration.py | 2 + testing/test_main.py | 2 + testing/test_mercurial.py | 5 ++- testing/test_regressions.py | 2 + testing/test_setuptools_support.py | 5 ++- testing/test_version.py | 4 +- testing/wd_wrapper.py | 17 ++++---- 40 files changed, 217 insertions(+), 208 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba8933a7..43e396d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: rev: v3.1.0 hooks: - id: reorder-python-imports - args: [ "--application-directories=.:src" , --py3-plus] + args: [ "--application-directories=.:src" , --py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.2.0 hooks: @@ -26,7 +26,7 @@ repos: rev: v2.32.0 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py37-plus] - repo: https://github.com/asottile/setup-cfg-fmt rev: v1.20.1 hooks: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 74f0f5ae..80b5523a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,10 @@ +v7.0.0 +======= + +* drop python 3.6 support +* include git archival support + + v6.4.3 ====== diff --git a/mypy.ini b/mypy.ini index 2d0a855c..fb092111 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,5 @@ [mypy] -python_version = 3.6 +python_version = 3.7 warn_return_any = True warn_unused_configs = True mypy_path = $MYPY_CONFIG_FILE_DIR/src @@ -7,4 +7,4 @@ mypy_path = $MYPY_CONFIG_FILE_DIR/src [mypy-setuptools_scm.*] # disabled as it will take a bit # disallow_untyped_defs = True -# strict = true +strict = true diff --git a/setup.cfg b/setup.cfg index fd090cd3..8f21eb8f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -31,7 +30,7 @@ install_requires = packaging>=20.0 setuptools tomli>=1.0.0 # keep in sync -python_requires = >=3.6 +python_requires = >=3.7 package_dir = =src zip_safe = true diff --git a/setup.py b/setup.py index c1627285..66670033 100644 --- a/setup.py +++ b/setup.py @@ -9,29 +9,15 @@ pip usage is recommended """ +from __future__ import annotations + import os import sys -from typing import NoReturn -from typing import Optional import setuptools -from setuptools.command.bdist_egg import bdist_egg as original_bdist_egg - - -class bdist_egg(original_bdist_egg): - def run(self) -> NoReturn: - raise SystemExit( - "%s is forbidden, " - "please update to setuptools>=45 which uses pip" % type(self).__name__ - ) def scm_version() -> str: - - if sys.version_info < (3, 6): - raise RuntimeError( - "support for python < 3.6 has been removed in setuptools_scm>=6.0.0" - ) here = os.path.dirname(os.path.abspath(__file__)) src = os.path.join(here, "src") @@ -46,7 +32,7 @@ def scm_version() -> str: from setuptools_scm.version import ScmVersion - def parse(root: str, config: Configuration) -> Optional[ScmVersion]: + def parse(root: str, config: Configuration) -> ScmVersion | None: try: return parse_pkginfo(root, config) except OSError: @@ -69,5 +55,4 @@ def parse(root: str, config: Configuration) -> Optional[ScmVersion]: ], "test": ["pytest>=6.2", "virtualenv>20"], }, - cmdclass={"bdist_egg": bdist_egg}, ) diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py index 9e53371f..6edf3e4c 100644 --- a/src/setuptools_scm/__init__.py +++ b/src/setuptools_scm/__init__.py @@ -2,13 +2,13 @@ :copyright: 2010-2015 by Ronny Pfannschmidt :license: MIT """ +from __future__ import annotations + import os import warnings from typing import Any from typing import Callable -from typing import Optional from typing import TYPE_CHECKING -from typing import Union from . import _types as _t from ._entrypoints import _call_entrypoint_fn @@ -46,7 +46,7 @@ } -def version_from_scm(root: _t.PathT) -> Optional[ScmVersion]: +def version_from_scm(root: _t.PathT) -> ScmVersion | None: warnings.warn( "version_from_scm is deprecated please use get_version", category=DeprecationWarning, @@ -60,7 +60,7 @@ def dump_version( root: _t.PathT, version: str, write_to: _t.PathT, - template: "str | None" = None, + template: str | None = None, ) -> None: assert isinstance(version, str) target = os.path.normpath(os.path.join(root, write_to)) @@ -79,7 +79,7 @@ def dump_version( fp.write(template.format(version=version, version_tuple=version_tuple)) -def _do_parse(config: Configuration) -> "ScmVersion|None": +def _do_parse(config: Configuration) -> ScmVersion | None: pretended = _read_pretended_version_for(config) if pretended is not None: return pretended @@ -90,7 +90,7 @@ def _do_parse(config: Configuration) -> "ScmVersion|None": raise TypeError( f"version parse result was {str!r}\nplease return a parsed version" ) - version: Optional[ScmVersion] + version: ScmVersion | None if parse_result: assert isinstance(parse_result, ScmVersion) version = parse_result @@ -105,7 +105,7 @@ def _do_parse(config: Configuration) -> "ScmVersion|None": return version -def _version_missing(config: Configuration) -> "NoReturn": +def _version_missing(config: Configuration) -> NoReturn: raise LookupError( f"setuptools-scm was unable to detect version for {config.absolute_root}.\n\n" "Make sure you're either building from a fully intact git repository " @@ -120,19 +120,19 @@ def _version_missing(config: Configuration) -> "NoReturn": def get_version( root: str = ".", - version_scheme: Union[Callable[[ScmVersion], str], str] = DEFAULT_VERSION_SCHEME, - local_scheme: Union[Callable[[ScmVersion], str], str] = DEFAULT_LOCAL_SCHEME, - write_to: Optional[_t.PathT] = None, - write_to_template: Optional[str] = None, - relative_to: Optional[str] = None, + version_scheme: Callable[[ScmVersion], str] | str = DEFAULT_VERSION_SCHEME, + local_scheme: Callable[[ScmVersion], str] | str = DEFAULT_LOCAL_SCHEME, + write_to: _t.PathT | None = None, + write_to_template: str | None = None, + relative_to: str | None = None, tag_regex: str = DEFAULT_TAG_REGEX, - parentdir_prefix_version: Optional[str] = None, - fallback_version: Optional[str] = None, + parentdir_prefix_version: str | None = None, + fallback_version: str | None = None, fallback_root: _t.PathT = ".", - parse: Optional[Any] = None, - git_describe_command: Optional[Any] = None, - dist_name: Optional[str] = None, - version_cls: Optional[Any] = None, + parse: Any | None = None, + git_describe_command: Any | None = None, + dist_name: str | None = None, + version_cls: Any | None = None, normalize: bool = True, search_parent_directories: bool = False, ) -> str: @@ -150,7 +150,7 @@ def get_version( return maybe_version -def _get_version(config: Configuration) -> "str|None": +def _get_version(config: Configuration) -> str | None: parsed_version = _do_parse(config) if parsed_version is None: return None diff --git a/src/setuptools_scm/__main__.py b/src/setuptools_scm/__main__.py index 1b616792..dab6068a 100644 --- a/src/setuptools_scm/__main__.py +++ b/src/setuptools_scm/__main__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ._cli import main if __name__ == "__main__": diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py index 0e0ad77b..88dd6d09 100644 --- a/src/setuptools_scm/_cli.py +++ b/src/setuptools_scm/_cli.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import os import sys diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index 299ddaa4..134ec4f9 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import warnings from typing import Any from typing import Iterator -from typing import Optional from typing import overload from typing import Protocol from typing import TYPE_CHECKING @@ -21,17 +22,17 @@ class MaybeConfigFunction(Protocol): __name__: str @overload - def __call__(self, root: _t.PathT, config: Configuration) -> Optional[ScmVersion]: + def __call__(self, root: _t.PathT, config: Configuration) -> ScmVersion | None: pass @overload - def __call__(self, root: _t.PathT) -> Optional[ScmVersion]: + def __call__(self, root: _t.PathT) -> ScmVersion | None: pass def _call_entrypoint_fn( root: _t.PathT, config: Configuration, fn: MaybeConfigFunction -) -> Optional[ScmVersion]: +) -> ScmVersion | None: if function_has_arg(fn, "config"): return fn(root, config=config) else: @@ -47,7 +48,7 @@ def _call_entrypoint_fn( def _version_from_entrypoints( config: Configuration, fallback: bool = False -) -> "ScmVersion|None": +) -> ScmVersion | None: if fallback: entrypoint = "setuptools_scm.parse_scm_fallback" root = config.fallback_root @@ -59,7 +60,7 @@ def _version_from_entrypoints( trace("version_from_ep", entrypoint, root) for ep in iter_matching_entrypoints(root, entrypoint, config): - version: Optional[ScmVersion] = _call_entrypoint_fn(root, config, ep.load()) + version: ScmVersion | None = _call_entrypoint_fn(root, config, ep.load()) trace(ep, version) if version: return version @@ -73,7 +74,7 @@ def _version_from_entrypoints( def iter_entry_points( - group: str, name: Optional[str] = None + group: str, name: str | None = None ) -> Iterator[_t.EntrypointProtocol]: all_eps = entry_points() if hasattr(all_eps, "select"): diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py index 00876e04..f18b82c0 100644 --- a/src/setuptools_scm/_overrides.py +++ b/src/setuptools_scm/_overrides.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import os -from typing import Optional from .config import Configuration from .utils import trace @@ -11,14 +12,14 @@ PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}" -def _read_pretended_version_for(config: Configuration) -> Optional[ScmVersion]: +def _read_pretended_version_for(config: Configuration) -> ScmVersion | None: """read a a overridden version from the environment tries ``SETUPTOOLS_SCM_PRETEND_VERSION`` and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME`` """ trace("dist name:", config.dist_name) - pretended: Optional[str] + pretended: str | None if config.dist_name is not None: pretended = os.environ.get( PRETEND_KEY_NAMED.format(name=config.dist_name.upper()) diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index 09fb2b65..7849eae1 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import sys from typing import Any diff --git a/src/setuptools_scm/_version_cls.py b/src/setuptools_scm/_version_cls.py index 7bde4b28..39e66b25 100644 --- a/src/setuptools_scm/_version_cls.py +++ b/src/setuptools_scm/_version_cls.py @@ -1,6 +1,6 @@ +from __future__ import annotations + from logging import getLogger -from typing import Tuple -from typing import Union from packaging.version import InvalidVersion from packaging.version import Version as Version @@ -31,7 +31,7 @@ def __repr__(self) -> str: return f"" -def _version_as_tuple(version_str: str) -> Tuple[Union[int, str], ...]: +def _version_as_tuple(version_str: str) -> tuple[int | str, ...]: try: parsed_version = Version(version_str) except InvalidVersion: @@ -40,7 +40,7 @@ def _version_as_tuple(version_str: str) -> Tuple[Union[int, str], ...]: log.exception("failed to parse version %s", version_str) return (version_str,) else: - version_fields: Tuple[Union[int, str], ...] = parsed_version.release + version_fields: tuple[int | str, ...] = parsed_version.release if parsed_version.dev is not None: version_fields += (f"dev{parsed_version.dev}",) if parsed_version.local is not None: diff --git a/src/setuptools_scm/config.py b/src/setuptools_scm/config.py index eba15e92..a8bccdf3 100644 --- a/src/setuptools_scm/config.py +++ b/src/setuptools_scm/config.py @@ -1,13 +1,12 @@ """ configuration """ +from __future__ import annotations + import os import re import warnings from typing import Any from typing import Callable -from typing import Dict -from typing import Optional from typing import Pattern -from typing import Type from typing import TYPE_CHECKING from typing import Union @@ -25,7 +24,7 @@ DEFAULT_LOCAL_SCHEME = "node-and-date" -def _check_tag_regex(value: Optional[Union[str, Pattern[str]]]) -> Pattern[str]: +def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]: if not value: value = DEFAULT_TAG_REGEX regex = re.compile(value) @@ -40,7 +39,7 @@ def _check_tag_regex(value: Optional[Union[str, Pattern[str]]]) -> Pattern[str]: return regex -def _check_absolute_root(root: _t.PathT, relative_to: Optional[_t.PathT]) -> str: +def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str: trace("abs root", repr(locals())) if relative_to: if ( @@ -65,7 +64,7 @@ def _check_absolute_root(root: _t.PathT, relative_to: Optional[_t.PathT]) -> str return os.path.abspath(root) -def _lazy_tomli_load(data: str) -> Dict[str, Any]: +def _lazy_tomli_load(data: str) -> dict[str, Any]: from tomli import loads return loads(data) @@ -77,31 +76,33 @@ def _lazy_tomli_load(data: str) -> Dict[str, Any]: class Configuration: """Global configuration model""" - parent: Optional[_t.PathT] + parent: _t.PathT | None _root: str - _relative_to: Optional[str] - version_cls: Type[VersionT] + _relative_to: str | None + version_cls: type[VersionT] def __init__( self, - relative_to: "_t.PathT | None" = None, + relative_to: _t.PathT | None = None, root: _t.PathT = ".", - version_scheme: Union[ - str, Callable[["ScmVersion"], Optional[str]] - ] = DEFAULT_VERSION_SCHEME, - local_scheme: Union[ - str, Callable[["ScmVersion"], Optional[str]] - ] = DEFAULT_LOCAL_SCHEME, - write_to: "_t.PathT | None" = None, - write_to_template: "str|None" = None, - tag_regex: "str | Pattern[str]" = DEFAULT_TAG_REGEX, - parentdir_prefix_version: "str|None" = None, - fallback_version: "str|None" = None, + version_scheme: ( + str | Callable[[ScmVersion], str | None] + ) = DEFAULT_VERSION_SCHEME, + local_scheme: (str | Callable[[ScmVersion], str | None]) = DEFAULT_LOCAL_SCHEME, + write_to: _t.PathT | None = None, + write_to_template: str | None = None, + tag_regex: str | Pattern[str] = DEFAULT_TAG_REGEX, + parentdir_prefix_version: str | None = None, + fallback_version: str | None = None, fallback_root: _t.PathT = ".", - parse: Optional[Any] = None, - git_describe_command: Optional[_t.CMD_TYPE] = None, - dist_name: "str|None" = None, - version_cls: "Type[Version]|Type[NonNormalizedVersion]|type|str|None" = None, + parse: Any | None = None, + git_describe_command: _t.CMD_TYPE | None = None, + dist_name: str | None = None, + version_cls: type[Version] + | type[NonNormalizedVersion] + | type + | str + | None = None, normalize: bool = True, search_parent_directories: bool = False, ): @@ -162,7 +163,7 @@ def absolute_root(self) -> str: return self._absolute_root @property - def relative_to(self) -> Optional[str]: + def relative_to(self) -> str | None: return self._relative_to @relative_to.setter @@ -188,17 +189,17 @@ def tag_regex(self) -> Pattern[str]: return self._tag_regex @tag_regex.setter - def tag_regex(self, value: Union[str, Pattern[str]]) -> None: + def tag_regex(self, value: str | Pattern[str]) -> None: self._tag_regex = _check_tag_regex(value) @classmethod def from_file( cls, name: str = "pyproject.toml", - dist_name: Optional[str] = None, - _load_toml: Callable[[str], Dict[str, Any]] = _lazy_tomli_load, + dist_name: str | None = None, + _load_toml: Callable[[str], dict[str, Any]] = _lazy_tomli_load, **kwargs: Any, - ) -> "Configuration": + ) -> Configuration: """ Read Configuration from pyproject.toml (or similar). Raises exceptions when file is not found or toml is @@ -231,7 +232,7 @@ def from_file( return cls(dist_name=dist_name, **section, **kwargs) -def _read_dist_name_from_setup_cfg() -> Optional[str]: +def _read_dist_name_from_setup_cfg() -> str | None: # minimal effort to read dist_name off setup.cfg metadata import configparser diff --git a/src/setuptools_scm/discover.py b/src/setuptools_scm/discover.py index 84fd3efd..87533fd5 100644 --- a/src/setuptools_scm/discover.py +++ b/src/setuptools_scm/discover.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from typing import Iterable from typing import Iterator diff --git a/src/setuptools_scm/file_finder.py b/src/setuptools_scm/file_finder.py index 741cb9eb..f4ef9bbf 100644 --- a/src/setuptools_scm/file_finder.py +++ b/src/setuptools_scm/file_finder.py @@ -1,4 +1,4 @@ -from __future__ import annotations # type: ignore +from __future__ import annotations import os from typing import TYPE_CHECKING diff --git a/src/setuptools_scm/file_finder_git.py b/src/setuptools_scm/file_finder_git.py index 922369a8..c7ac5daf 100644 --- a/src/setuptools_scm/file_finder_git.py +++ b/src/setuptools_scm/file_finder_git.py @@ -1,12 +1,10 @@ +from __future__ import annotations + import logging import os import subprocess import tarfile from typing import IO -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple from . import _types as _t from .file_finder import is_toplevel_acceptable @@ -17,7 +15,7 @@ log = logging.getLogger(__name__) -def _git_toplevel(path: str) -> Optional[str]: +def _git_toplevel(path: str) -> str | None: try: cwd = os.path.abspath(path or ".") out, err, ret = do_ex(["git", "rev-parse", "HEAD"], cwd=cwd) @@ -54,7 +52,7 @@ def _git_toplevel(path: str) -> Optional[str]: return None -def _git_interpret_archive(fd: IO[bytes], toplevel: str) -> Tuple[Set[str], Set[str]]: +def _git_interpret_archive(fd: IO[bytes], toplevel: str) -> tuple[set[str], set[str]]: with tarfile.open(fileobj=fd, mode="r|*") as tf: git_files = set() git_dirs = {toplevel} @@ -67,7 +65,7 @@ def _git_interpret_archive(fd: IO[bytes], toplevel: str) -> Tuple[Set[str], Set[ return git_files, git_dirs -def _git_ls_files_and_dirs(toplevel: str) -> Tuple[Set[str], Set[str]]: +def _git_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: # use git archive instead of git ls-file to honor # export-ignore git attribute @@ -89,7 +87,7 @@ def _git_ls_files_and_dirs(toplevel: str) -> Tuple[Set[str], Set[str]]: return set(), set() -def git_find_files(path: _t.PathT = "") -> List[str]: +def git_find_files(path: _t.PathT = "") -> list[str]: toplevel = _git_toplevel(os.fspath(path)) if not is_toplevel_acceptable(toplevel): return [] diff --git a/src/setuptools_scm/file_finder_hg.py b/src/setuptools_scm/file_finder_hg.py index 35231793..4f5e3ec2 100644 --- a/src/setuptools_scm/file_finder_hg.py +++ b/src/setuptools_scm/file_finder_hg.py @@ -1,24 +1,21 @@ +from __future__ import annotations + import os import subprocess -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple from .file_finder import is_toplevel_acceptable from .file_finder import scm_find_files from .utils import do_ex -def _hg_toplevel(path: str) -> Optional[str]: +def _hg_toplevel(path: str) -> str | None: try: - with open(os.devnull, "wb") as devnull: - out = subprocess.check_output( - ["hg", "root"], - cwd=(path or "."), - universal_newlines=True, - stderr=devnull, - ) + out: str = subprocess.check_output( + ["hg", "root"], + cwd=(path or "."), + text=True, + stderr=subprocess.DEVNULL, + ) return os.path.normcase(os.path.realpath(out.strip())) except subprocess.CalledProcessError: # hg returned error, we are not in a mercurial repo @@ -28,8 +25,8 @@ def _hg_toplevel(path: str) -> Optional[str]: return None -def _hg_ls_files_and_dirs(toplevel: str) -> Tuple[Set[str], Set[str]]: - hg_files: Set[str] = set() +def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: + hg_files: set[str] = set() hg_dirs = {toplevel} out, err, ret = do_ex(["hg", "files"], cwd=toplevel) if ret: @@ -45,7 +42,7 @@ def _hg_ls_files_and_dirs(toplevel: str) -> Tuple[Set[str], Set[str]]: return hg_files, hg_dirs -def hg_find_files(path: str = "") -> List[str]: +def hg_find_files(path: str = "") -> list[str]: toplevel = _hg_toplevel(path) if not is_toplevel_acceptable(toplevel): return [] diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 875d80a9..e417be02 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import re import warnings @@ -7,12 +9,7 @@ from os.path import join from os.path import samefile from typing import Callable -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple from typing import TYPE_CHECKING -from typing import Union from . import _types as _t from .config import Configuration @@ -51,7 +48,7 @@ class GitWorkdir(Workdir): COMMAND = "git" @classmethod - def from_potential_worktree(cls, wd: _t.PathT) -> Optional["GitWorkdir"]: + def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None: require_command(cls.COMMAND) wd = os.path.abspath(wd) real_wd, _, ret = do_ex("git rev-parse --show-prefix", wd) @@ -76,7 +73,7 @@ def is_dirty(self) -> bool: out, _, _ = self.do_ex("git status --porcelain --untracked-files=no") return bool(out) - def get_branch(self) -> Optional[str]: + 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) @@ -86,7 +83,7 @@ def get_branch(self) -> Optional[str]: return None return branch - def get_head_date(self) -> Optional[date]: + def get_head_date(self) -> date | None: timestamp, err, ret = self.do_ex( "git -c log.showSignature=false log -n 1 HEAD --format=%cI" ) @@ -106,7 +103,7 @@ def is_shallow(self) -> bool: def fetch_shallow(self) -> None: self.do_ex("git fetch --unshallow") - def node(self) -> Optional[str]: + def node(self) -> str | None: node, _, ret = self.do_ex("git rev-parse --verify --quiet HEAD") if not ret: return node[:7] @@ -142,7 +139,7 @@ def fail_on_shallow(wd: GitWorkdir) -> None: ) -def get_working_directory(config: Configuration) -> Optional[GitWorkdir]: +def get_working_directory(config: Configuration) -> GitWorkdir | None: """ Return the working directory (``GitWorkdir``). """ @@ -158,10 +155,10 @@ def get_working_directory(config: Configuration) -> Optional[GitWorkdir]: def parse( root: str, - describe_command: Optional[Union[str, List[str]]] = None, + describe_command: str | list[str] | None = None, pre_parse: Callable[[GitWorkdir], None] = warn_on_shallow, - config: Optional[Configuration] = None, -) -> Optional[ScmVersion]: + config: Configuration | None = None, +) -> ScmVersion | None: """ :param pre_parse: experimental pre_parse action, may change at any time """ @@ -179,11 +176,9 @@ def parse( def _git_parse_inner( config: Configuration, - wd: Union[GitWorkdir, "GitWorkdirHgClient"], - pre_parse: Optional[ - Callable[[Union[GitWorkdir, "GitWorkdirHgClient"]], None] - ] = None, - describe_command: Optional[_t.CMD_TYPE] = None, + wd: GitWorkdir | GitWorkdirHgClient, + pre_parse: None | (Callable[[GitWorkdir | GitWorkdirHgClient], None]) = None, + describe_command: _t.CMD_TYPE | None = None, ) -> ScmVersion: if pre_parse: pre_parse(wd) @@ -195,8 +190,8 @@ def _git_parse_inner( out, _, ret = wd.do_ex(describe_command) else: out, _, ret = wd.default_describe() - distance: Optional[int] - node: Optional[str] + distance: int | None + node: str | None if ret == 0: tag, distance, node, dirty = _git_parse_describe(out) if distance == 0 and not dirty: @@ -226,7 +221,7 @@ def _git_parse_inner( ) -def _git_parse_describe(describe_output: str) -> Tuple[str, int, str, bool]: +def _git_parse_describe(describe_output: str) -> tuple[str, int, str, bool]: # 'describe_output' looks e.g. like 'v1.5.0-0-g4060507' or # 'v1.15.1rc1-37-g9bd1298-dirty'. @@ -240,7 +235,7 @@ def _git_parse_describe(describe_output: str) -> Tuple[str, int, str, bool]: return tag, int(number), node, dirty -def search_parent(dirname: _t.PathT) -> Optional[GitWorkdir]: +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. @@ -269,7 +264,7 @@ def search_parent(dirname: _t.PathT) -> Optional[GitWorkdir]: def archival_to_version( - data: Dict[str, str], config: Optional[Configuration] = None + data: dict[str, str], config: Configuration | None = None ) -> ScmVersion: trace("data", data) archival_describe = data.get("describe-name", DESCRIBE_UNSUPPORTED) @@ -291,8 +286,8 @@ def archival_to_version( def parse_archival( - root: _t.PathT, config: Optional[Configuration] = None -) -> Optional[ScmVersion]: + root: _t.PathT, config: Configuration | None = None +) -> ScmVersion | None: archival = os.path.join(root, ".git_archival.txt") data = data_from_mime(archival) return archival_to_version(data, config=config) diff --git a/src/setuptools_scm/hacks.py b/src/setuptools_scm/hacks.py index 259bab27..494691dd 100644 --- a/src/setuptools_scm/hacks.py +++ b/src/setuptools_scm/hacks.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import os -from typing import Optional from . import _types as _t from .config import Configuration @@ -13,8 +14,8 @@ def parse_pkginfo( - root: _t.PathT, config: Optional[Configuration] = None -) -> Optional[ScmVersion]: + root: _t.PathT, config: Configuration | None = None +) -> ScmVersion | None: pkginfo = os.path.join(root, "PKG-INFO") trace("pkginfo", pkginfo) @@ -27,8 +28,8 @@ def parse_pkginfo( def parse_pip_egg_info( - root: _t.PathT, config: Optional[Configuration] = None -) -> Optional[ScmVersion]: + root: _t.PathT, config: Configuration | None = None +) -> ScmVersion | None: pipdir = os.path.join(root, "pip-egg-info") if not os.path.isdir(pipdir): return None @@ -39,7 +40,7 @@ def parse_pip_egg_info( return parse_pkginfo(os.path.join(pipdir, items[0]), config=config) -def fallback_version(root: _t.PathT, config: Configuration) -> Optional[ScmVersion]: +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)) if parent_name.startswith(config.parentdir_prefix_version): diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index fad874be..fb310661 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import datetime import os from pathlib import Path -from typing import Dict -from typing import Optional from . import _types as _t from ._version_cls import Version @@ -22,14 +22,14 @@ class HgWorkdir(Workdir): COMMAND = "hg" @classmethod - def from_potential_worktree(cls, wd: _t.PathT) -> "HgWorkdir | None": + def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None: require_command(cls.COMMAND) root, err, ret = do_ex("hg root", wd) if ret: return None return cls(root) - def get_meta(self, config: Configuration) -> Optional[ScmVersion]: + def get_meta(self, config: Configuration) -> ScmVersion | None: node: str tags_str: str @@ -107,7 +107,7 @@ def hg_log(self, revset: str, template: str) -> str: cmd = ["hg", "log", "-r", revset, "-T", template] return self.do(cmd) - def get_latest_normalizable_tag(self) -> Optional[str]: + def get_latest_normalizable_tag(self) -> str | None: # Gets all tags containing a '.' (see #229) from oldest to newest outlines = self.hg_log( revset="ancestors(.) and tag('re:\\.')", @@ -124,7 +124,7 @@ def get_distance_revs(self, rev1: str, rev2: str = ".") -> int: out = self.hg_log(revset, ".") return len(out) - 1 - def check_changes_since_tag(self, tag: Optional[str]) -> bool: + def check_changes_since_tag(self, tag: str | None) -> bool: if tag == "0.0" or tag is None: return True @@ -140,7 +140,7 @@ def check_changes_since_tag(self, tag: Optional[str]) -> bool: return bool(self.hg_log(revset, ".")) -def parse(root: _t.PathT, config: "Configuration|None" = None) -> Optional[ScmVersion]: +def parse(root: _t.PathT, config: Configuration | None = None) -> ScmVersion | None: if not config: config = Configuration(root=root) @@ -167,7 +167,7 @@ def parse(root: _t.PathT, config: "Configuration|None" = None) -> Optional[ScmVe def archival_to_version( - data: Dict[str, str], config: "Configuration | None" = None + data: dict[str, str], config: Configuration | None = None ) -> ScmVersion: trace("data", data) node = data.get("node", "")[:12] @@ -186,9 +186,7 @@ def archival_to_version( return meta("0.0", node=node, config=config) -def parse_archival( - root: _t.PathT, config: Optional[Configuration] = None -) -> ScmVersion: +def parse_archival(root: _t.PathT, config: Configuration | None = None) -> ScmVersion: archival = os.path.join(root, ".hg_archival.txt") data = data_from_mime(archival) return archival_to_version(data, config=config) diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index be843674..28c5a361 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -1,9 +1,8 @@ +from __future__ import annotations + import os from datetime import date from datetime import datetime -from typing import Dict -from typing import Optional -from typing import Set from . import _types as _t from .git import GitWorkdir @@ -20,7 +19,7 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir): COMMAND = "hg" @classmethod - def from_potential_worktree(cls, wd: _t.PathT) -> "GitWorkdirHgClient | None": + def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None: require_command(cls.COMMAND) root, _, ret = do_ex("hg root", wd) if ret: @@ -31,14 +30,14 @@ def is_dirty(self) -> bool: out, _, _ = self.do_ex("hg id -T '{dirty}'") return bool(out) - def get_branch(self) -> Optional[str]: + def get_branch(self) -> str | None: res = self.do_ex("hg id -T {bookmarks}") if res.returncode: trace("branch err", res) return None return res.out - def get_head_date(self) -> Optional[date]: + 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) @@ -51,14 +50,14 @@ def is_shallow(self) -> bool: def fetch_shallow(self) -> None: pass - def get_hg_node(self) -> "str | None": + def get_hg_node(self) -> str | None: node, _, ret = self.do_ex("hg log -r . -T {node}") if not ret: return node else: return None - def _hg2git(self, hg_node: str) -> "str | None": + def _hg2git(self, hg_node: str) -> str | None: git_node = None with open(os.path.join(self.path, ".hg/git-mapfile")) as file: for line in file: @@ -67,7 +66,7 @@ def _hg2git(self, hg_node: str) -> "str | None": break return git_node - def node(self) -> "str | None": + def node(self) -> str | None: hg_node = self.get_hg_node() if hg_node is None: return None @@ -113,18 +112,18 @@ def default_describe(self) -> _t.CmdResult: ) if ret: return _FAKE_GIT_DESCRIBE_ERROR - hg_tags: Set[str] = set(hg_tags_str.split()) + hg_tags: set[str] = set(hg_tags_str.split()) if not hg_tags: return _FAKE_GIT_DESCRIBE_ERROR - node: "str | None" = None + node: str | None = None with open(os.path.join(self.path, ".hg/git-tags")) as fp: - git_tags: Dict[str, str] = dict(line.split() for line in fp) + git_tags: dict[str, str] = dict(line.split() for line in fp) - tag: "str | None" = next( + tag: str | None = next( # find the first hg tag which is also a git tag (tag for tag in hg_tags if tag in git_tags), None, diff --git a/src/setuptools_scm/integration.py b/src/setuptools_scm/integration.py index 53df404f..1fddc539 100644 --- a/src/setuptools_scm/integration.py +++ b/src/setuptools_scm/integration.py @@ -1,9 +1,9 @@ +from __future__ import annotations + import os import warnings from typing import Any from typing import Callable -from typing import Dict -from typing import List import setuptools @@ -62,7 +62,7 @@ def _assign_version(dist: setuptools.Distribution, config: Configuration) -> Non def version_keyword( dist: setuptools.Distribution, keyword: str, - value: "bool | Dict[str, Any] | Callable[[], Dict[str, Any]]", + value: bool | dict[str, Any] | Callable[[], dict[str, Any]], ) -> None: if not value: return @@ -85,7 +85,7 @@ def version_keyword( _assign_version(dist, config) -def find_files(path: _t.PathT = "") -> List[str]: +def find_files(path: _t.PathT = "") -> list[str]: for ep in iter_entry_points("setuptools_scm.files_command"): command = ep.load() if isinstance(command, str): diff --git a/src/setuptools_scm/scm_workdir.py b/src/setuptools_scm/scm_workdir.py index 2d5dc173..45ab7228 100644 --- a/src/setuptools_scm/scm_workdir.py +++ b/src/setuptools_scm/scm_workdir.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import ClassVar from . import _types as _t diff --git a/src/setuptools_scm/utils.py b/src/setuptools_scm/utils.py index 73a988c8..d1a02fc9 100644 --- a/src/setuptools_scm/utils.py +++ b/src/setuptools_scm/utils.py @@ -1,6 +1,8 @@ """ utils """ +from __future__ import annotations + import inspect import os import platform @@ -8,12 +10,8 @@ import subprocess import sys import warnings -from os import _Environ -from typing import Dict from typing import Iterator -from typing import List -from typing import Optional -from typing import Union +from typing import Mapping from . import _types as _t @@ -21,7 +19,7 @@ IS_WINDOWS = platform.system() == "Windows" -def no_git_env(env: Union[Dict[str, str], _Environ[str]]) -> Dict[str, str]: +def no_git_env(env: Mapping[str, str]) -> dict[str, str]: # adapted from pre-commit # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 @@ -47,7 +45,7 @@ def trace(*k: object) -> None: print(*k, file=sys.stderr, flush=True) -def ensure_stripped_str(str_or_bytes: "str | bytes") -> str: +def ensure_stripped_str(str_or_bytes: str | bytes) -> str: if isinstance(str_or_bytes, str): return str_or_bytes.strip() else: @@ -90,14 +88,14 @@ def do_ex(cmd: _t.CMD_TYPE, cwd: _t.PathT = ".") -> _t.CmdResult: ) -def do(cmd: "List[str] | str", cwd: "str | _t.PathT" = ".") -> str: +def do(cmd: list[str] | str, cwd: str | _t.PathT = ".") -> str: out, err, ret = do_ex(cmd, cwd) if ret: print(err) return out -def data_from_mime(path: _t.PathT) -> Dict[str, str]: +def data_from_mime(path: _t.PathT) -> dict[str, str]: with open(path, encoding="utf-8") as fp: content = fp.read() trace("content", repr(content)) @@ -115,7 +113,7 @@ def function_has_arg(fn: object, argname: str) -> bool: return argname in argspec -def has_command(name: str, args: "List[str] | None" = None, warn: bool = True) -> bool: +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 = _popen_pipes(cmd, ".") @@ -136,7 +134,7 @@ def require_command(name: str) -> None: def iter_entry_points( - group: str, name: Optional[str] = None + group: str, name: str | None = None ) -> Iterator[_t.EntrypointProtocol]: from ._entrypoints import iter_entry_points diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index f7796649..4938d2af 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -1,4 +1,4 @@ -from __future__ import annotations # type: ignore +from __future__ import annotations import os import re diff --git a/testing/check_self_install.py b/testing/check_self_install.py index 6a326878..0cfab33a 100644 --- a/testing/check_self_install.py +++ b/testing/check_self_install.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pkg_resources import setuptools_scm diff --git a/testing/conftest.py b/testing/conftest.py index ab9b6417..89f124dc 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,9 +1,9 @@ +from __future__ import annotations + import os from pathlib import Path from typing import Any from typing import Generator -from typing import List -from typing import Tuple import pytest @@ -16,7 +16,7 @@ VERSION_PKGS = ["setuptools", "setuptools_scm"] -def pytest_report_header() -> List[str]: +def pytest_report_header() -> list[str]: try: from importlib.metadata import version # type: ignore except ImportError: @@ -53,7 +53,7 @@ def wd(tmp_path: Path) -> WorkDir: @pytest.fixture -def repositories_hg_git(tmp_path: Path) -> Tuple[WorkDir, WorkDir]: +def repositories_hg_git(tmp_path: Path) -> tuple[WorkDir, WorkDir]: from setuptools_scm.utils import do tmp_path = tmp_path.resolve() diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py index ef79fca1..ba11a23a 100644 --- a/testing/test_basic_api.py +++ b/testing/test_basic_api.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import shutil import sys diff --git a/testing/test_config.py b/testing/test_config.py index 53c034a7..d7aa3f47 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re import textwrap from pathlib import Path diff --git a/testing/test_file_finder.py b/testing/test_file_finder.py index ec233499..a7304343 100644 --- a/testing/test_file_finder.py +++ b/testing/test_file_finder.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import os import sys from typing import Generator from typing import Iterable -from typing import Set import pytest @@ -44,7 +45,7 @@ def inwd( yield wd -def _sep(paths: Iterable[str]) -> Set[str]: +def _sep(paths: Iterable[str]) -> set[str]: return {path.replace("/", os.path.sep) for path in paths} diff --git a/testing/test_functions.py b/testing/test_functions.py index fc62b5f5..53bc92b1 100644 --- a/testing/test_functions.py +++ b/testing/test_functions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path import pytest diff --git a/testing/test_git.py b/testing/test_git.py index 27bccce3..8394f7a1 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import sys from datetime import date @@ -6,7 +8,6 @@ from os.path import join as opj from pathlib import Path from textwrap import dedent -from typing import Dict from unittest.mock import Mock from unittest.mock import patch @@ -144,7 +145,7 @@ def test_version_from_git(wd: WorkDir) -> None: ) -setup_py_with_normalize: Dict[str, str] = { +setup_py_with_normalize: dict[str, str] = { "false": """ from setuptools import setup setup(use_scm_version={'normalize': False, 'write_to': 'VERSION.txt'}) @@ -475,7 +476,7 @@ def test_git_getdate_signed_commit(signed_commit_wd: WorkDir) -> None: ], ) @pytest.mark.filterwarnings("ignore:git archive did not support describe output") -def test_git_archival_to_version(expected: str, from_data: Dict[str, str]) -> None: +def test_git_archival_to_version(expected: str, from_data: dict[str, str]) -> None: config = Configuration() version = archival_to_version(from_data, config=config) assert ( diff --git a/testing/test_hg_git.py b/testing/test_hg_git.py index 1271b721..cf64bbed 100644 --- a/testing/test_hg_git.py +++ b/testing/test_hg_git.py @@ -1,4 +1,4 @@ -from typing import Tuple +from __future__ import annotations import pytest @@ -23,7 +23,7 @@ def _check_hg_git() -> None: pytest.skip("hg-git not installed") -def test_base(repositories_hg_git: Tuple[WorkDir, WorkDir]) -> None: +def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None: wd, wd_git = repositories_hg_git assert wd_git.version == "0.1.dev0" diff --git a/testing/test_integration.py b/testing/test_integration.py index 9759fa83..25a0469f 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import sys import textwrap diff --git a/testing/test_main.py b/testing/test_main.py index 41588bb7..10d78c8e 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import sys import textwrap diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 0b77e377..5e0bc027 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import os from pathlib import Path -from typing import Dict import pytest @@ -40,7 +41,7 @@ def wd(wd: WorkDir) -> WorkDir: @pytest.mark.parametrize("expected,data", sorted(archival_mapping.items())) -def test_archival_to_version(expected: str, data: Dict[str, str]) -> None: +def test_archival_to_version(expected: str, data: dict[str, str]) -> None: config = Configuration() version = archival_to_version(data, config=config) assert ( diff --git a/testing/test_regressions.py b/testing/test_regressions.py index c7168579..6de71410 100644 --- a/testing/test_regressions.py +++ b/testing/test_regressions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import subprocess import sys diff --git a/testing/test_setuptools_support.py b/testing/test_setuptools_support.py index f6954520..212b4811 100644 --- a/testing/test_setuptools_support.py +++ b/testing/test_setuptools_support.py @@ -1,13 +1,14 @@ """ integration tests that check setuptools version support """ +from __future__ import annotations + import os import pathlib import subprocess import sys from typing import Any from typing import Callable -from typing import List import _pytest.config import pytest @@ -182,7 +183,7 @@ def test_on_old_setuptools( # monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG", raising=False) - def run_and_output(cmd: "List[str | pathlib.Path]") -> bytes: + def run_and_output(cmd: list[str | pathlib.Path]) -> bytes: res = subprocess.run(cmd, cwd=str(pkg), stdout=subprocess.PIPE) if not res.returncode: return res.stdout.strip() diff --git a/testing/test_version.py b/testing/test_version.py index aecceb64..0a070959 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from datetime import date from datetime import timedelta from typing import Any @@ -222,7 +224,7 @@ def test_format_version_schemes() -> None: def date_to_str( - date_: "date | None" = None, + date_: date | None = None, days_offset: int = 0, fmt: str = "{dt:%y}.{dt.month}.{dt.day}", ) -> str: diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py index 3fbdd1d2..de578c42 100644 --- a/testing/wd_wrapper.py +++ b/testing/wd_wrapper.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import itertools from pathlib import Path from typing import Any -from typing import List class WorkDir: @@ -18,7 +19,7 @@ def __init__(self, cwd: Path) -> None: self.cwd = cwd self.__counter = itertools.count() - def __call__(self, cmd: "List[str] | str", **kw: object) -> str: + def __call__(self, cmd: list[str] | str, **kw: object) -> str: if kw: assert isinstance(cmd, str), "formatting the command requires text input" cmd = cmd.format(**kw) @@ -26,7 +27,7 @@ def __call__(self, cmd: "List[str] | str", **kw: object) -> str: return do(cmd, self.cwd) - def write(self, name: str, content: "str | bytes", **kw: object) -> Path: + def write(self, name: str, content: str | bytes, **kw: object) -> Path: path = self.cwd / name if kw: assert isinstance(content, str) @@ -37,28 +38,26 @@ def write(self, name: str, content: "str | bytes", **kw: object) -> Path: path.write_text(content) return path - def _reason(self, given_reason: "str | None") -> str: + def _reason(self, given_reason: str | None) -> str: if given_reason is None: return f"number-{next(self.__counter)}" else: return given_reason def add_and_commit( - self, reason: "str | None" = None, signed: bool = False, **kwargs: object + self, reason: str | None = None, signed: bool = False, **kwargs: object ) -> None: self(self.add_command) self.commit(reason=reason, signed=signed, **kwargs) - def commit(self, reason: "str | None" = None, signed: bool = False) -> None: + def commit(self, reason: str | None = None, signed: bool = False) -> None: reason = self._reason(reason) self( self.commit_command if not signed else self.signed_commit_command, reason=reason, ) - def commit_testfile( - self, reason: "str | None" = None, signed: bool = False - ) -> None: + 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(self.add_command) From 5aa9c8eab1c151b4cc9393bc0c649fb5c33059f6 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 24 May 2022 10:35:07 +0200 Subject: [PATCH 09/18] reviews part 2 --- src/setuptools_scm/config.py | 65 ++++++++++++++++++----------------- src/setuptools_scm/utils.py | 40 ++++++++++----------- src/setuptools_scm/version.py | 16 ++++----- tox.ini | 2 +- 4 files changed, 61 insertions(+), 62 deletions(-) diff --git a/src/setuptools_scm/config.py b/src/setuptools_scm/config.py index a8bccdf3..1dbd4564 100644 --- a/src/setuptools_scm/config.py +++ b/src/setuptools_scm/config.py @@ -6,6 +6,7 @@ import warnings from typing import Any from typing import Callable +from typing import cast from typing import Pattern from typing import TYPE_CHECKING from typing import Union @@ -70,7 +71,36 @@ def _lazy_tomli_load(data: str) -> dict[str, Any]: return loads(data) -VersionT = Union[Version, NonNormalizedVersion] +_VersionT = Union[Version, NonNormalizedVersion] + + +def _validate_version_cls( + version_cls: type[_VersionT] | str | None, normalize: bool +) -> type[_VersionT]: + if not normalize: + # `normalize = False` means `version_cls = NonNormalizedVersion` + if version_cls is not None: + raise ValueError( + "Providing a custom `version_cls` is not permitted when " + "`normalize=False`" + ) + return NonNormalizedVersion + else: + # Use `version_cls` if provided, default to packaging or pkg_resources + if version_cls is None: + return Version + elif isinstance(version_cls, str): + try: + # Not sure this will work in old python + import importlib + + pkg, cls_name = version_cls.rsplit(".", 1) + version_cls_host = importlib.import_module(pkg) + return cast(type[_VersionT], getattr(version_cls_host, cls_name)) + except: # noqa + raise ValueError(f"Unable to import version_cls='{version_cls}'") + else: + return version_cls class Configuration: @@ -79,7 +109,7 @@ class Configuration: parent: _t.PathT | None _root: str _relative_to: str | None - version_cls: type[VersionT] + version_cls: type[_VersionT] def __init__( self, @@ -98,11 +128,7 @@ def __init__( parse: Any | None = None, git_describe_command: _t.CMD_TYPE | None = None, dist_name: str | None = None, - version_cls: type[Version] - | type[NonNormalizedVersion] - | type - | str - | None = None, + version_cls: type[_VersionT] | type | str | None = None, normalize: bool = True, search_parent_directories: bool = False, ): @@ -125,30 +151,7 @@ def __init__( self.search_parent_directories = search_parent_directories self.parent = None - if not normalize: - # `normalize = False` means `version_cls = NonNormalizedVersion` - if version_cls is not None: - raise ValueError( - "Providing a custom `version_cls` is not permitted when " - "`normalize=False`" - ) - self.version_cls = NonNormalizedVersion - else: - # Use `version_cls` if provided, default to packaging or pkg_resources - if version_cls is None: - self.version_cls = Version - elif isinstance(version_cls, str): - try: - # Not sure this will work in old python - import importlib - - pkg, cls_name = version_cls.rsplit(".", 1) - version_cls_host = importlib.import_module(pkg) - self.version_cls = getattr(version_cls_host, cls_name) - except: # noqa - raise ValueError(f"Unable to import version_cls='{version_cls}'") - else: - self.version_cls = version_cls + self.version_cls = _validate_version_cls(version_cls, normalize) @property def fallback_root(self) -> str: diff --git a/src/setuptools_scm/utils.py b/src/setuptools_scm/utils.py index d1a02fc9..1f4df32e 100644 --- a/src/setuptools_scm/utils.py +++ b/src/setuptools_scm/utils.py @@ -3,13 +3,14 @@ """ from __future__ import annotations -import inspect import os import platform import shlex import subprocess import sys import warnings +from types import CodeType +from types import FunctionType from typing import Iterator from typing import Mapping @@ -52,11 +53,10 @@ def ensure_stripped_str(str_or_bytes: str | bytes) -> str: return str_or_bytes.decode("utf-8", "surrogateescape").strip() -def _popen_pipes(cmd: _t.CMD_TYPE, cwd: _t.PathT) -> subprocess.Popen[bytes]: - return subprocess.Popen( +def _run(cmd: _t.CMD_TYPE, cwd: _t.PathT) -> subprocess.CompletedProcess[bytes]: + return subprocess.run( cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, cwd=str(cwd), env=dict( no_git_env(os.environ), @@ -75,16 +75,15 @@ def do_ex(cmd: _t.CMD_TYPE, cwd: _t.PathT = ".") -> _t.CmdResult: if os.name == "posix" and not isinstance(cmd, (list, tuple)): cmd = shlex.split(cmd) - p = _popen_pipes(cmd, cwd) - out, err = p.communicate() - if out: - trace("out", repr(out)) - if err: - trace("err", repr(err)) - if p.returncode: - trace("ret", p.returncode) + res = _run(cmd, cwd) + if res.stdout: + trace("out", repr(res.stdout)) + if res.stderr: + trace("err", repr(res.stderr)) + if res.returncode: + trace("ret", res.returncode) return _t.CmdResult( - ensure_stripped_str(out), ensure_stripped_str(err), p.returncode + ensure_stripped_str(res.stdout), ensure_stripped_str(res.stderr), res.returncode ) @@ -105,23 +104,20 @@ def data_from_mime(path: _t.PathT) -> dict[str, str]: return data -def function_has_arg(fn: object, argname: str) -> bool: - assert inspect.isfunction(fn) - - argspec = inspect.signature(fn).parameters - - return argname in argspec +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 = _popen_pipes(cmd, ".") + p = _run(cmd, ".") except OSError: trace(*sys.exc_info()) res = False else: - p.communicate() res = not p.returncode if not res and warn: warnings.warn("%r was not found" % name, category=RuntimeWarning) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 4938d2af..2086b454 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -22,7 +22,7 @@ from . import _types as _t from ._version_cls import Version as PkgVersion from .config import Configuration -from .config import VersionT +from .config import _VersionT from .utils import trace SEMVER_MINOR = 2 @@ -67,8 +67,8 @@ def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any: def tag_to_version( - tag: VersionT | str, config: Configuration | None = None -) -> VersionT | None: + tag: _VersionT | str, config: Configuration | None = None +) -> _VersionT | None: """ take a tag that might be prefixed with a keyword and return only the version part :param config: optional configuration object @@ -101,13 +101,13 @@ def tag_to_version( def tags_to_versions( tags: list[str], config: Configuration | None = None -) -> list[VersionT]: +) -> list[_VersionT]: """ take tags that might be prefixed with a keyword and return only the version part :param tags: an iterable of tags :param config: optional configuration object """ - result: list[VersionT] = [] + result: list[_VersionT] = [] for tag in tags: parsed = tag_to_version(tag, config=config) if parsed: @@ -192,8 +192,8 @@ def format_next_version( def _parse_tag( - tag: VersionT | str, preformatted: bool, config: Configuration | None -) -> VersionT | str: + tag: _VersionT | str, preformatted: bool, config: Configuration | None +) -> _VersionT | str: if preformatted: return tag elif config is None or not isinstance(tag, config.version_cls): @@ -205,7 +205,7 @@ def _parse_tag( def meta( - tag: str | VersionT, + tag: str | _VersionT, distance: int | None = None, dirty: bool = False, node: str | None = None, diff --git a/tox.ini b/tox.ini index 65edad1f..4d64cbb8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py{36,37,38,39,310}-{test,selfcheck},check_readme,check-dist +envlist=py{37,38,39,310}-{test,selfcheck},check_readme,check-dist [pytest] testpaths=testing From fb31eca873e04d8ecccacc2028a2e2d379d0e218 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 29 May 2022 12:05:10 +0200 Subject: [PATCH 10/18] update pre-commit hook --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43e396d5..b961f356 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.32.1 hooks: - id: pyupgrade args: [--py37-plus] @@ -32,7 +32,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.950' + rev: 'v0.960' hooks: - id: mypy args: [--strict] From ffb409bc18bc0978d57834168efd5a68dee9d55e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 29 May 2022 12:07:46 +0200 Subject: [PATCH 11/18] fix #714 - use sys.executable instead of plain python --- testing/test_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_integration.py b/testing/test_integration.py index 25a0469f..99970685 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -132,7 +132,8 @@ def test_pretend_version_accepts_bad_string( monkeypatch.setenv(PRETEND_KEY, "dummy") wd.write("setup.py", SETUP_PY_PLAIN) assert wd.get_version(write_to="test.py") == "dummy" - assert wd("python setup.py --version") == "0.0.0" + pyver = wd([sys.executable, "setup.py", "--version"]) + assert pyver == "0.0.0" def test_own_setup_fails_on_old_python(monkeypatch: pytest.MonkeyPatch) -> None: From 6e46ba490ab48715d4a76d6d614cd7d0703233bb Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 29 May 2022 12:32:32 +0200 Subject: [PATCH 12/18] add importlib_metadata for mypy pre-commit hook --- .pre-commit-config.yaml | 1 + src/setuptools_scm/_entrypoints.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b961f356..f09daa06 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,3 +41,4 @@ repos: - types-setuptools - tokenize-rt==3.2.0 - pytest == 7.1 + - importlib_metadata diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index 134ec4f9..e50991b7 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -70,7 +70,7 @@ def _version_from_entrypoints( try: from importlib.metadata import entry_points # type: ignore except ImportError: - from importlib_metadata import entry_points # type: ignore + from importlib_metadata import entry_points def iter_entry_points( From 20c5c53af0e7b7db12c8c2798fa7f0829c9af4d9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 29 May 2022 12:32:55 +0200 Subject: [PATCH 13/18] remove unsupported python from the pipeline --- .github/workflows/python-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 76344da7..affabe95 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: [ '3.6', '3.7', '3.8', '3.9', '3.10', 'pypy-3.6' ] + python_version: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy-3.8' ] os: [windows-latest, ubuntu-latest] #, macos-latest] include: - os: windows-latest From d3d2573dfd2529f288d01c9b6256db1778397213 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 29 May 2022 12:43:18 +0200 Subject: [PATCH 14/18] restore python3.7 support --- pyproject.toml | 4 +++- setup.cfg | 1 + src/setuptools_scm/_entrypoints.py | 3 ++- src/setuptools_scm/_types.py | 9 ++------- tox.ini | 1 + 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c066bbc..03f9636c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,8 @@ requires = [ "setuptools>=45", "tomli>=1.0", - "packaging>=20.0" + "packaging>=20.0", + "typing_extensions", + "importlib_metadata", ] build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index 8f21eb8f..88deace2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ install_requires = packaging>=20.0 setuptools tomli>=1.0.0 # keep in sync + typing-extensions python_requires = >=3.7 package_dir = =src diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index e50991b7..0b86b90a 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -4,9 +4,10 @@ from typing import Any from typing import Iterator from typing import overload -from typing import Protocol from typing import TYPE_CHECKING +from typing_extensions import Protocol + from . import _types as _t from .utils import function_has_arg from .utils import trace diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index 7849eae1..555b1e75 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -1,12 +1,9 @@ from __future__ import annotations -import os -import sys from typing import Any from typing import Callable from typing import List from typing import NamedTuple -from typing import Protocol from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -14,11 +11,9 @@ if TYPE_CHECKING: from setuptools_scm import version + import os -if sys.version_info >= (3, 9): - from typing import ParamSpec, TypeAlias -else: - from typing_extensions import ParamSpec, TypeAlias +from typing_extensions import ParamSpec, TypeAlias, Protocol PathT = Union["os.PathLike[str]", str] diff --git a/tox.ini b/tox.ini index 4d64cbb8..e26ee80a 100644 --- a/tox.ini +++ b/tox.ini @@ -29,6 +29,7 @@ deps= setuptools >= 45 tomli virtualenv>20 + typing_extensions commands= test: pytest [] selfcheck: python setup.py --version From 46426e91796d2d27646b310b0df5dc730e0ab3f7 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 29 May 2022 21:44:30 +0200 Subject: [PATCH 15/18] use typing.Type for python3.7 --- src/setuptools_scm/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/setuptools_scm/config.py b/src/setuptools_scm/config.py index 1dbd4564..f5c0c1ab 100644 --- a/src/setuptools_scm/config.py +++ b/src/setuptools_scm/config.py @@ -8,6 +8,7 @@ from typing import Callable from typing import cast from typing import Pattern +from typing import Type from typing import TYPE_CHECKING from typing import Union @@ -96,7 +97,7 @@ def _validate_version_cls( pkg, cls_name = version_cls.rsplit(".", 1) version_cls_host = importlib.import_module(pkg) - return cast(type[_VersionT], getattr(version_cls_host, cls_name)) + return cast(Type[_VersionT], getattr(version_cls_host, cls_name)) except: # noqa raise ValueError(f"Unable to import version_cls='{version_cls}'") else: From 97af84d79eea17f22bc99e432e3ee47edd81123e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 29 May 2022 21:49:56 +0200 Subject: [PATCH 16/18] remove integration test for setup.py error as annotations are only supported on 3.7 --- testing/test_integration.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/testing/test_integration.py b/testing/test_integration.py index 99970685..939bd4b6 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import sys import textwrap from pathlib import Path @@ -136,19 +135,6 @@ def test_pretend_version_accepts_bad_string( assert pyver == "0.0.0" -def test_own_setup_fails_on_old_python(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr("sys.version_info", (3, 5)) - monkeypatch.syspath_prepend(os.path.dirname(os.path.dirname(__file__))) - - import setup - - with pytest.raises( - RuntimeError, - match="support for python < 3.6 has been removed in setuptools_scm>=6.0.0", - ): - setup.scm_version() - - def testwarn_on_broken_setuptools() -> None: _warn_on_old_setuptools("45") with pytest.warns(RuntimeWarning, match="ERROR: setuptools==44"): From 56a9d52d78d943eacbb200a31d31da2059dd992b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 29 May 2022 22:39:16 +0200 Subject: [PATCH 17/18] fix issues in gl pipeline --- .github/workflows/python-tests.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index affabe95..5b96eafb 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy-3.8' ] + python_version: ['3.7', '3.8', '3.9', '3.10', '3.11.0-alpha - 3.11.0', 'pypy-3.8' ] os: [windows-latest, ubuntu-latest] #, macos-latest] include: - os: windows-latest @@ -28,9 +28,9 @@ jobs: name: ${{ matrix.os }} - Python ${{ matrix.python_version }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 if: matrix.python_version != 'msys2' with: python-version: ${{ matrix.python_version }} @@ -83,7 +83,7 @@ jobs: - name: Setup python uses: actions/setup-python@v2 with: - python-version: "3.6" + python-version: "3.7" 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 @@ -92,11 +92,11 @@ jobs: strategy: fail-fast: false matrix: - python_version: [ '3.6', '3.9', 'pypy-3.6' ] + python_version: [ '3.7', '3.9', 'pypy-3.8' ] installer: ["pip install"] name: check self install - Python ${{ matrix.python_version }} via ${{ matrix.installer }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Setup python uses: actions/setup-python@v2 with: @@ -105,7 +105,7 @@ jobs: # self install testing needs some clarity # so its being executed without any other tools running # setuptools smaller 52 is needed to do easy_install - - run: pip install -U "setuptools<52" tomli packaging + - run: pip install -U "setuptools<52" tomli packaging typing_extensions importlib_metadata - run: python setup.py egg_info - run: python setup.py sdist - run: ${{ matrix.installer }} dist/* From 6707c4fde80b222ff9b98e26da4736d0eeb9560d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 19 Jun 2022 22:38:24 +0200 Subject: [PATCH 18/18] FIXUP: restore hg-git support --- src/setuptools_scm/git.py | 4 ++-- src/setuptools_scm/hg_git.py | 43 +++++++++++++++++------------------ src/setuptools_scm/version.py | 3 ++- testing/conftest.py | 11 +++++---- testing/test_hg_git.py | 3 ++- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index e417be02..18a9de34 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -74,10 +74,10 @@ def is_dirty(self) -> bool: return bool(out) def get_branch(self) -> str | None: - branch, err, ret = self.do_ex("git rev-parse --abbrev-ref HEAD") + 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") + branch, err, ret = self.do_ex("git symbolic-ref --short HEAD --") if ret: trace("branch err (symbolic-ref)", branch, err, ret) return None diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index 28c5a361..3f860e96 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from contextlib import suppress from datetime import date from datetime import datetime @@ -21,7 +22,7 @@ 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) + root, _, ret = do_ex(["hg", "root"], wd) if ret: return None return cls(root) @@ -58,13 +59,13 @@ def get_hg_node(self) -> str | None: return None def _hg2git(self, hg_node: str) -> str | None: - git_node = None - with open(os.path.join(self.path, ".hg/git-mapfile")) as file: - for line in file: - if hg_node in line: - git_node, hg_node = line.split() - break - return git_node + with suppress(FileNotFoundError): + with open(os.path.join(self.path, ".hg/git-mapfile")) as map_items: + for item in map_items: + if hg_node in item: + git_node, hg_node = item.split() + return git_node + return None def node(self) -> str | None: hg_node = self.get_hg_node() @@ -90,7 +91,7 @@ def node(self) -> str | None: return git_node[:7] def count_all_nodes(self) -> int: - revs, _, _ = self.do_ex("hg log -r 'ancestors(.)' -T '.'") + revs, _, _ = self.do_ex(["hg", "log", "-r", "ancestors(.)", "-T", "."]) return len(revs) def default_describe(self) -> _t.CmdResult: @@ -105,30 +106,28 @@ def default_describe(self) -> _t.CmdResult: "hg", "log", "-r", - "(reverse(ancestors(.)) and tag(r're:[0-9]'))", + "(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))", "-T", "{tags}{if(tags, ' ', '')}", ] ) if ret: return _FAKE_GIT_DESCRIBE_ERROR - hg_tags: set[str] = set(hg_tags_str.split()) + hg_tags: list[str] = hg_tags_str.split() if not hg_tags: return _FAKE_GIT_DESCRIBE_ERROR - node: str | None = None - with open(os.path.join(self.path, ".hg/git-tags")) as fp: + git_tags: dict[str, str] = dict(line.split()[::-1] for line in fp) - git_tags: dict[str, str] = dict(line.split() for line in fp) - - tag: str | None = next( - # find the first hg tag which is also a git tag - (tag for tag in hg_tags if tag in git_tags), - None, - ) - if tag is None: + tag: str + for hg_tag in hg_tags: + if hg_tag in git_tags: + tag = hg_tag + break + else: + trace("tag not found", hg_tags, git_tags) return _FAKE_GIT_DESCRIBE_ERROR out, _, ret = self.do_ex(["hg", "log", "-r", f"'{tag}'::.", "-T", "."]) @@ -142,5 +141,5 @@ def default_describe(self) -> _t.CmdResult: if self.is_dirty(): desc += "-dirty" - + trace("desc", desc) return _t.CmdResult(desc, "", 0) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 2086b454..528171f9 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -162,7 +162,8 @@ def exact(self) -> bool: def __repr__(self) -> str: return self.format_with( - "" + "" ) def format_with(self, fmt: str, **kw: object) -> str: diff --git a/testing/conftest.py b/testing/conftest.py index 89f124dc..c8810428 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -10,10 +10,13 @@ from .wd_wrapper import WorkDir -# 2009-02-13T23:31:30+00:00 -os.environ["SOURCE_DATE_EPOCH"] = "1234567890" -os.environ["SETUPTOOLS_SCM_DEBUG"] = "1" -VERSION_PKGS = ["setuptools", "setuptools_scm"] +def pytest_configure() -> None: + # 2009-02-13T23:31:30+00:00 + os.environ["SOURCE_DATE_EPOCH"] = "1234567890" + os.environ["SETUPTOOLS_SCM_DEBUG"] = "1" + + +VERSION_PKGS = ["setuptools", "setuptools_scm", "packaging"] def pytest_report_header() -> list[str]: diff --git a/testing/test_hg_git.py b/testing/test_hg_git.py index cf64bbed..476016f4 100644 --- a/testing/test_hg_git.py +++ b/testing/test_hg_git.py @@ -30,9 +30,10 @@ def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None: assert wd.version == "0.1.dev0" wd_git.commit_testfile() + version_git = wd_git.version + wd("hg pull -u") - version_git = wd_git.version version = wd.version assert version_git.startswith("0.1.dev1+g")