Repository: pypa/setuptools_scm
Pull request #634 (2021-09-14 21:04:03)

build/test fixes
- enhance mercurial detection in hg_git tests
- drop devpi testenv
- drop flake8 env for pre-commit
- trim down the toml extra and only make it need modern setuptools
- add test requirements as extra
- run tox selfcheck on all tox python versions
- drop unneeded flake8 excludes
- use build to build the artifacts in the chec_dist env
- drop the dist env, setup.py shouldn't be used for that
- minimal acceptance test for dummy pretend versions



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 384fb14..f4dd514 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,8 @@
+6.3.3
+======
+
+* only put minimal setuptools version into toml extra to warn people with old strict pins
+
 6.3.2
 =====
 
diff --git a/setup.cfg b/setup.cfg
index 3fe4541..e11f276 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -68,8 +68,3 @@ setuptools_scm.version_scheme =
     release-branch-semver = setuptools_scm.version:release_branch_semver_version
     no-guess-dev = setuptools_scm.version:no_guess_dev_version
     calver-by-date = setuptools_scm.version:calver_by_date
-
-[options.extras_require]
-toml =
-    setuptools>=42
-    tomli>=1.0.0
diff --git a/setup.py b/setup.py
index a2ba8fa..763708e 100644
--- a/setup.py
+++ b/setup.py
@@ -38,14 +38,13 @@ def scm_version():
     from setuptools_scm import get_version
     from setuptools_scm.hacks import parse_pkginfo
     from setuptools_scm import git
-    from setuptools_scm import hg
     from setuptools_scm.version import guess_next_dev_version, get_local_node_and_date
 
     def parse(root, config):
         try:
             return parse_pkginfo(root, config)
         except OSError:
-            return git.parse(root, config=config) or hg.parse(root, config=config)
+            return git.parse(root, config=config)
 
     return get_version(
         root=here,
@@ -57,13 +56,12 @@ def scm_version():
 
 if __name__ == "__main__":
     setuptools.setup(
-        setup_requires=["setuptools"],
         version=scm_version(),
         extras_require={
             "toml": [
                 "setuptools>=42",
-                "tomli>=1.0.0",
             ],
+            "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 b4f86ea..8d4a36c 100644
--- a/src/setuptools_scm/__init__.py
+++ b/src/setuptools_scm/__init__.py
@@ -5,6 +5,12 @@
 import os
 import warnings
 
+from ._entrypoints import _call_entrypoint_fn
+from ._entrypoints import _version_from_entrypoints
+from ._overrides import _read_pretended_version_for
+from ._overrides import PRETEND_KEY
+from ._overrides import PRETEND_KEY_NAMED
+from ._version_cls import _version_as_tuple
 from ._version_cls import NonNormalizedVersion
 from ._version_cls import Version
 from .config import Configuration
@@ -17,9 +23,6 @@ from .utils import trace
 from .version import format_version
 from .version import meta
 
-PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
-PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}"
-
 TEMPLATES = {
     ".py": """\
 # coding: utf-8
@@ -43,36 +46,7 @@ def version_from_scm(root):
     return _version_from_entrypoints(config)
 
 
-def _call_entrypoint_fn(root, config, fn):
-    if function_has_arg(fn, "config"):
-        return fn(root, config=config)
-    else:
-        warnings.warn(
-            f"parse function {fn.__module__}.{fn.__name__}"
-            " are required to provide a named argument"
-            " 'config', setuptools_scm>=8.0 will remove support.",
-            category=DeprecationWarning,
-            stacklevel=2,
-        )
-        return fn(root)
-
-
-def _version_from_entrypoints(config: Configuration, fallback=False):
-    if fallback:
-        entrypoint = "setuptools_scm.parse_scm_fallback"
-        root = config.fallback_root
-    else:
-        entrypoint = "setuptools_scm.parse_scm"
-        root = config.absolute_root
-
-    for ep in iter_matching_entrypoints(root, entrypoint, config):
-        version = _call_entrypoint_fn(root, config, ep.load())
-        trace(ep, version)
-        if version:
-            return version
-
-
-def dump_version(root, version, write_to, template=None):
+def dump_version(root, version: str, write_to, template: "str | None" = None):
     assert isinstance(version, str)
     if not write_to:
         return
@@ -86,35 +60,16 @@ def dump_version(root, version, write_to, template=None):
                 os.path.splitext(target)[1], target
             )
         )
-
-    parsed_version = Version(version)
-    version_fields = parsed_version.release
-    if parsed_version.dev is not None:
-        version_fields += (f"dev{parsed_version.dev}",)
-    if parsed_version.local is not None:
-        version_fields += (parsed_version.local,)
+    version_tuple = _version_as_tuple(version)
 
     with open(target, "w") as fp:
-        fp.write(template.format(version=version, version_tuple=tuple(version_fields)))
+        fp.write(template.format(version=version, version_tuple=version_tuple))
 
 
 def _do_parse(config):
-
-    trace("dist name:", config.dist_name)
-    if config.dist_name is not None:
-        pretended = os.environ.get(
-            PRETEND_KEY_NAMED.format(name=config.dist_name.upper())
-        )
-    else:
-        pretended = None
-
-    if pretended is None:
-        pretended = os.environ.get(PRETEND_KEY)
-
-    if pretended:
-        # we use meta here since the pretended version
-        # must adhere to the pep to begin with
-        return meta(tag=pretended, preformatted=True, config=config)
+    pretended = _read_pretended_version_for(config)
+    if pretended is not None:
+        return pretended
 
     if config.parse:
         parse_result = _call_entrypoint_fn(config.absolute_root, config, config.parse)
@@ -201,6 +156,8 @@ __all__ = [
     "DEFAULT_VERSION_SCHEME",
     "DEFAULT_LOCAL_SCHEME",
     "DEFAULT_TAG_REGEX",
+    "PRETEND_KEY",
+    "PRETEND_KEY_NAMED",
     "Version",
     "NonNormalizedVersion",
     # TODO: are the symbols below part of public API ?
diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py
new file mode 100644
index 0000000..48510e2
--- /dev/null
+++ b/src/setuptools_scm/_entrypoints.py
@@ -0,0 +1,53 @@
+import warnings
+from typing import Optional
+
+from .config import Configuration
+from .discover import iter_matching_entrypoints
+from .utils import function_has_arg
+from .utils import trace
+
+
+def _call_entrypoint_fn(root, config, fn):
+    if function_has_arg(fn, "config"):
+        return fn(root, config=config)
+    else:
+        warnings.warn(
+            f"parse function {fn.__module__}.{fn.__name__}"
+            " are required to provide a named argument"
+            " 'config', setuptools_scm>=8.0 will remove support.",
+            category=DeprecationWarning,
+            stacklevel=2,
+        )
+        return fn(root)
+
+
+def _version_from_entrypoints(config: Configuration, fallback=False):
+    if fallback:
+        entrypoint = "setuptools_scm.parse_scm_fallback"
+        root = config.fallback_root
+    else:
+        entrypoint = "setuptools_scm.parse_scm"
+        root = config.absolute_root
+
+    for ep in iter_matching_entrypoints(root, entrypoint, config):
+        version = _call_entrypoint_fn(root, config, ep.load())
+        trace(ep, version)
+        if version:
+            return version
+
+
+try:
+    from importlib.metadata import entry_points  # type: ignore
+except ImportError:
+    from pkg_resources import iter_entry_points
+else:
+
+    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)
diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py
new file mode 100644
index 0000000..292936c
--- /dev/null
+++ b/src/setuptools_scm/_overrides.py
@@ -0,0 +1,37 @@
+import os
+from typing import Optional
+
+from .config import Configuration
+from .utils import trace
+from .version import meta
+from .version import ScmVersion
+
+
+PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
+PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}"
+
+
+def _read_pretended_version_for(config: Configuration) -> Optional[ScmVersion]:
+    """read a a overriden 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]
+    if config.dist_name is not None:
+        pretended = os.environ.get(
+            PRETEND_KEY_NAMED.format(name=config.dist_name.upper())
+        )
+    else:
+        pretended = None
+
+    if pretended is None:
+        pretended = os.environ.get(PRETEND_KEY)
+
+    if pretended is not None:
+        # we use meta here since the pretended version
+        # must adhere to the pep to begin with
+        return meta(tag=pretended, preformatted=True, config=config)
+    else:
+        return None
diff --git a/src/setuptools_scm/_version_cls.py b/src/setuptools_scm/_version_cls.py
index 0cefb26..d6a2b69 100644
--- a/src/setuptools_scm/_version_cls.py
+++ b/src/setuptools_scm/_version_cls.py
@@ -1,9 +1,17 @@
+from logging import getLogger
+from typing import Tuple
+
 try:
-    from packaging.version import Version
+    from packaging.version import Version, InvalidVersion
 
-    assert hasattr(Version, "release")
+    assert hasattr(
+        Version, "release"
+    ), "broken installation ensure packaging>=20 is available"
 except ImportError:
-    from pkg_resources._vendor.packaging.version import Version as SetuptoolsVersion
+    from pkg_resources._vendor.packaging.version import (
+        Version as SetuptoolsVersion,
+        InvalidVersion,
+    )
 
     try:
         SetuptoolsVersion.release
@@ -47,3 +55,20 @@ class NonNormalizedVersion(Version):
     def __repr__(self):
         # same pattern as parent
         return f"<NonNormalizedVersion({self._raw_version!r})>"
+
+
+def _version_as_tuple(version_str) -> Tuple["int | str", ...]:
+    try:
+        parsed_version = Version(version_str)
+    except InvalidVersion:
+
+        log = getLogger("setuptools_scm")
+        log.exception("failed to parse version %s", version_str)
+        return (version_str,)
+    else:
+        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:
+            version_fields += (parsed_version.local,)
+        return version_fields
diff --git a/src/setuptools_scm/utils.py b/src/setuptools_scm/utils.py
index 2e84f87..2d200b3 100644
--- a/src/setuptools_scm/utils.py
+++ b/src/setuptools_scm/utils.py
@@ -8,7 +8,7 @@ import shlex
 import subprocess
 import sys
 import warnings
-from typing import Optional
+
 
 DEBUG = bool(os.environ.get("SETUPTOOLS_SCM_DEBUG"))
 IS_WINDOWS = platform.system() == "Windows"
@@ -137,18 +137,8 @@ def require_command(name):
         raise OSError("%r was not found" % name)
 
 
-try:
-    from importlib.metadata import entry_points  # type: ignore
-except ImportError:
-    from pkg_resources import iter_entry_points
-else:
-
-    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(*k, **kw):
+
+    from ._entrypoints import iter_entry_points
+
+    return iter_entry_points(*k, **kw)
diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py
index 91e25f6..1d4ddfc 100644
--- a/src/setuptools_scm/version.py
+++ b/src/setuptools_scm/version.py
@@ -177,7 +177,7 @@ def meta(
     branch: "str|None" = None,
     config: "Configuration|None" = None,
     **kw,
-):
+) -> ScmVersion:
     if not config:
         warnings.warn(
             "meta invoked without explicit configuration,"
diff --git a/tox.ini b/tox.ini
index 0f161ac..3f77ea9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist=py{36,37,38,39,310}-test,flake8,check_readme,check-dist,py{37}-selfcheck,docs
+envlist=py{36,37,38,39,310}-{test,selfcheck},check_readme,check-dist
 
 [pytest]
 testpaths=testing
@@ -16,14 +16,7 @@ addopts = -p no:unraisableexception
 max-complexity = 10
 max-line-length = 88
 ignore=E203,W503
-exclude=
-	.git,
-	.tox,
-	.env,
-	.venv,
-	.pytest_cache,
-	__pycache__,
-	./src/setuptools_scm/win_py31_compat.py
+
 
 [testenv]
 usedevelop=True
@@ -34,19 +27,12 @@ deps=
     pytest
     setuptools >= 45
     tomli
+    virtualenv>20
 commands=
     test: pytest []
     selfcheck: python setup.py --version
-extras =
-    toml
 
-[testenv:flake8]
-skip_install=True
-deps=
-    flake8
-    mccabe
-commands =
-    flake8 src/setuptools_scm/ testing/ setup.py
+
 
 [testenv:check_readme]
 skip_install=True
@@ -61,30 +47,12 @@ commands=
 
 [testenv:check_dist]
 deps=
-    wheel
+    build
     twine
 commands=
-    python setup.py clean --all rotate -k 0 -m .whl,.tar.gz,.zip
-    python setup.py -q egg_info
-    python setup.py -q sdist --formats zip bdist_wheel
+    python -m build
     twine check dist/*
 
 
 
-[testenv:dist]
-deps= wheel
-whitelist_externals = rm
-commands=
-    python setup.py -q clean --all
-    python setup.py -q rotate -k 0 -m .egg,.zip,.whl,.tar.gz
-    python setup.py -q egg_info
-    python setup.py -q sdist --formats zip,bztar bdist_wheel upload
-
-[testenv:devpi]
-deps=
-    devpi-client
-commands =
-    python setup.py -q egg_info
-    devpi upload --from-dir dist
-
 #XXX: envs for hg versions


Test patch:
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index 7963fc4..638242e 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -38,9 +38,9 @@ jobs:
           msystem: MINGW64
           install: git mingw-w64-x86_64-python mingw-w64-x86_64-python-setuptools
           update: true
-      - run: pip install -U setuptools
+      - run: pip install -U 'setuptools>=45'
         if: matrix.python_version != 'msys2'
-      - run: pip install -e .[toml] pytest virtualenv
+      - run: pip install -e .[toml,test]
       # pip2 is needed because Mercurial uses python2 on Ubuntu 20.04
       - run: |
           curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py
@@ -58,7 +58,7 @@ jobs:
         with:
           python-version: "3.6"
           architecture: x64
-      - run: pip install -e .[toml] pytest virtualenv
+      - run: pip install -e .[toml,test] pytest virtualenv
       - run: pytest --test-legacy testing/test_setuptools_support.py || true # ignore fail flaky on ci
   check_selfinstall:
     runs-on: ubuntu-latest
diff --git a/testing/test_hg_git.py b/testing/test_hg_git.py
index acae7b0..3a60d96 100644
--- a/testing/test_hg_git.py
+++ b/testing/test_hg_git.py
@@ -3,18 +3,23 @@ import pytest
 from setuptools_scm.utils import do_ex
 from setuptools_scm.utils import has_command
 
-python_hg, err, ret = do_ex("hg debuginstall --template {pythonexe}")
 
-if ret:
-    skip_no_hggit = True
-else:
-    out, err, ret = do_ex([python_hg.strip(), "-c", "import hggit"])
-    print(out, err, ret)
+@pytest.fixture(scope="module", autouse=True)
+def _check_hg_git():
+    if not has_command("hg", warn=False):
+        pytest.skip("hg executable not found")
+
+    python_hg, err, ret = do_ex("hg debuginstall --template {pythonexe}")
+
+    if ret:
+        skip_no_hggit = True
+    else:
+        out, err, ret = do_ex([python_hg.strip(), "-c", "import hggit"])
     skip_no_hggit = bool(ret)
+    if skip_no_hggit:
+        pytest.skip("hg-git not installed")
 
 
-@pytest.mark.skipif(not has_command("hg", warn=False), reason="hg executable not found")
-@pytest.mark.skipif(skip_no_hggit, reason="hg-git not installed")
 def test_base(repositories_hg_git):
     wd, wd_git = repositories_hg_git
 
diff --git a/testing/test_integration.py b/testing/test_integration.py
index c21d8bc..c0a2297 100644
--- a/testing/test_integration.py
+++ b/testing/test_integration.py
@@ -115,6 +115,13 @@ def test_pretend_version_name_takes_precedence(tmpdir, monkeypatch, wd):
     assert wd.get_version(dist_name="test") == "1.0.0"
 
 
+def test_pretend_version_accepts_bad_string(monkeypatch, wd):
+    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):
     monkeypatch.setattr("sys.version_info", (3, 5))
     monkeypatch.syspath_prepend(os.path.dirname(os.path.dirname(__file__)))