Repository: pypa/setuptools_scm

Issue:
pip 23.0.1 and anyio==4.0.0 break installation with --upgrade --no-binary :all: options
# Description

When trying to install dependency `anyio`, `setuptools_scm` installation is broken when using options  `--upgrade --no-binary :all:`
`pip` version is 23.0.1
Error observed:
```

LookupError: https://files.pythonhosted.org/packages/a7/16/8aef42ae27036ceb201f1774940357da18ae7daac45bdd721da554892012/setuptools-scm-8.0.1.tar.gz (from https://pypi.org/simple/setuptools-scm/) (requires-python:>=3.8) is already being built: setuptools_scm>=6.4 from https://files.pythonhosted.org/packages/a7/16/8aef42ae27036ceb201f1774940357da18ae7daac45bdd721da554892012/setuptools-scm-8.0.1.tar.gz
```

# Steps to reproduce
```
pip3 install --upgrade --no-binary :all: anyio==4.0.0
Collecting anyio==4.0.0
  Using cached anyio-4.0.0.tar.gz (153 kB)
  Installing build dependencies ... error
  error: subprocess-exited-with-error
```
<details><summary>Full log</summary>

```
$ pip3 install --upgrade --no-binary :all: anyio==4.0.0
DEPRECATION: --no-binary currently disables reading from the cache of locally built wheels. In the future --no-binary will not influence the wheel cache. pip 23.1 will enforce this behaviour change. A possible replacement is to use the --no-cache-dir option. You can use the flag --use-feature=no-binary-enable-wheel-cache to test the upcoming behaviour. Discussion can be found at https://github.com/pypa/pip/issues/11453
Collecting anyio==4.0.0
  Using cached anyio-4.0.0.tar.gz (153 kB)
  Installing build dependencies ... error
  error: subprocess-exited-with-error
  
  × pip subprocess to install build dependencies did not run successfully.
  │ exit code: 1
  ╰─> [116 lines of output]
      DEPRECATION: --no-binary currently disables reading from the cache of locally built wheels. In the future --no-binary will not influence the wheel cache. pip 23.1 will enforce this behaviour change. A possible replacement is to use the --no-cache-dir option. You can use the flag --use-feature=no-binary-enable-wheel-cache to test the upcoming behaviour. Discussion can be found at https://github.com/pypa/pip/issues/11453
      Collecting setuptools>=64
        Using cached setuptools-68.2.2.tar.gz (2.2 MB)
        Getting requirements to build wheel: started
        Getting requirements to build wheel: finished with status 'done'
        Installing backend dependencies: started
        Installing backend dependencies: finished with status 'done'
        Preparing metadata (pyproject.toml): started
        Preparing metadata (pyproject.toml): finished with status 'done'
      Collecting setuptools_scm>=6.4
        Using cached setuptools-scm-8.0.1.tar.gz (71 kB)
        Installing build dependencies: started
        Installing build dependencies: finished with status 'error'
        error: subprocess-exited-with-error
      
        × pip subprocess to install build dependencies did not run successfully.
        │ exit code: 1
        ╰─> [85 lines of output]
            DEPRECATION: --no-binary currently disables reading from the cache of locally built wheels. In the future --no-binary will not influence the wheel cache. pip 23.1 will enforce this behaviour change. A possible replacement is to use the --no-cache-dir option. You can use the flag --use-feature=no-binary-enable-wheel-cache to test the upcoming behaviour. Discussion can be found at https://github.com/pypa/pip/issues/11453
            Ignoring typing_extensions: markers 'python_version < "3.8"' don't match your environment
            Collecting importlib-metadata>=4.6
              Using cached importlib_metadata-6.8.0.tar.gz (53 kB)
              Installing build dependencies: started
              Installing build dependencies: finished with status 'error'
              error: subprocess-exited-with-error
      
              × pip subprocess to install build dependencies did not run successfully.
              │ exit code: 2
              ╰─> [61 lines of output]
                  DEPRECATION: --no-binary currently disables reading from the cache of locally built wheels. In the future --no-binary will not influence the wheel cache. pip 23.1 will enforce this behaviour change. A possible replacement is to use the --no-cache-dir option. You can use the flag --use-feature=no-binary-enable-wheel-cache to test the upcoming behaviour. Discussion can be found at https://github.com/pypa/pip/issues/11453
                  Collecting setuptools>=56
                    Using cached setuptools-68.2.2.tar.gz (2.2 MB)
                    Getting requirements to build wheel: started
                    Getting requirements to build wheel: finished with status 'done'
                    Installing backend dependencies: started
                    Installing backend dependencies: finished with status 'done'
                    Preparing metadata (pyproject.toml): started
                    Preparing metadata (pyproject.toml): finished with status 'done'
                  Collecting setuptools_scm[toml]>=3.4.1
                    Using cached setuptools-scm-8.0.1.tar.gz (71 kB)
                  ERROR: Exception:
                  Traceback (most recent call last):
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/cli/base_command.py", line 160, in exc_logging_wrapper
                      status = run_func(*args)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/cli/req_command.py", line 247, in wrapper
                      return func(self, options, args)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/commands/install.py", line 419, in run
                      requirement_set = resolver.resolve(
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/resolution/resolvelib/resolver.py", line 92, in resolve
                      result = self._result = resolver.resolve(
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_vendor/resolvelib/resolvers.py", line 481, in resolve
                      state = resolution.resolve(requirements, max_rounds=max_rounds)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_vendor/resolvelib/resolvers.py", line 348, in resolve
                      self._add_to_criteria(self.state.criteria, r, parent=None)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_vendor/resolvelib/resolvers.py", line 172, in _add_to_criteria
                      if not criterion.candidates:
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_vendor/resolvelib/structs.py", line 151, in __bool__
                      return bool(self._sequence)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 155, in __bool__
                      return any(self)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 143, in <genexpr>
                      return (c for c in iterator if id(c) not in self._incompatible_ids)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 47, in _iter_built
                      candidate = func()
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 206, in _make_candidate_from_link
                      self._link_candidate_cache[link] = LinkCandidate(
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 297, in __init__
                      super().__init__(
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 162, in __init__
                      self.dist = self._prepare()
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 231, in _prepare
                      dist = self._prepare_distribution()
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 308, in _prepare_distribution
                      return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/operations/prepare.py", line 491, in prepare_linked_requirement
                      return self._prepare_linked_requirement(req, parallel_builds)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/operations/prepare.py", line 577, in _prepare_linked_requirement
                      dist = _get_prepared_distribution(
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/operations/prepare.py", line 68, in _get_prepared_distribution
                      with build_tracker.track(req):
                    File "/usr/lib/python3.8/contextlib.py", line 113, in __enter__
                      return next(self.gen)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/operations/build/build_tracker.py", line 122, in track
                      self.add(req)
                    File "/home/ichvets/testenv/lib/python3.8/site-packages/pip/_internal/operations/build/build_tracker.py", line 92, in add
                      raise LookupError(message)
                  LookupError: https://files.pythonhosted.org/packages/a7/16/8aef42ae27036ceb201f1774940357da18ae7daac45bdd721da554892012/setuptools-scm-8.0.1.tar.gz (from https://pypi.org/simple/setuptools-scm/) (requires-python:>=3.8) is already being built: setuptools_scm>=6.4 from https://files.pythonhosted.org/packages/a7/16/8aef42ae27036ceb201f1774940357da18ae7daac45bdd721da554892012/setuptools-scm-8.0.1.tar.gz
      
                  [notice] A new release of pip is available: 23.0.1 -> 23.2.1
                  [notice] To update, run: pip install --upgrade pip
                  [end of output]
      
              note: This error originates from a subprocess, and is likely not a problem with pip.
            error: subprocess-exited-with-error
      
            × pip subprocess to install build dependencies did not run successfully.
            │ exit code: 2
            ╰─> See above for output.
      
            note: This error originates from a subprocess, and is likely not a problem with pip.
      
            [notice] A new release of pip is available: 23.0.1 -> 23.2.1
            [notice] To update, run: pip install --upgrade pip
            [end of output]
      
        note: This error originates from a subprocess, and is likely not a problem with pip.
      error: subprocess-exited-with-error
      
      × pip subprocess to install build dependencies did not run successfully.
      │ exit code: 1
      ╰─> See above for output.
      
      note: This error originates from a subprocess, and is likely not a problem with pip.
      
      [notice] A new release of pip is available: 23.0.1 -> 23.2.1
      [notice] To update, run: pip install --upgrade pip
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error

× pip subprocess to install build dependencies did not run successfully.
│ exit code: 1
╰─> See above for output.

note: This error originates from a subprocess, and is likely not a problem with pip.
```

</details>

# Expected behaviour

setuptools_scm is installed with no error messages.

# Workaround

Not available.

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index cc663fd..6e9212d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,9 @@
+v8.0.3
+======
+
+* fix #918 for good - remove external importlib-metadata to avoid source only loop
+* fix #926: ensure mypy on python3.8 works with the version file
+
 v8.0.2
 ======
 
diff --git a/pyproject.toml b/pyproject.toml
index 983789f..9a792b4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,11 +3,9 @@
 [build-system]
 build-backend = "_own_version_helper"
 requires = [
-  'importlib-metadata>=4.6; python_version < "3.10"',
   "rich",
   "setuptools>=61",
   'tomli; python_version < "3.11"',
-  'typing_extensions; python_version < "3.8"',
 ]
 backend-path = [
   ".",
@@ -42,7 +40,6 @@ dynamic = [
   "version",
 ]
 dependencies = [
-  'importlib-metadata>=4.6; python_version < "3.10"',
   "packaging>=20",
   "setuptools",
   'tomli>=1; python_version < "3.11"',
diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py
index 3cb9590..be6fddd 100644
--- a/src/setuptools_scm/_entrypoints.py
+++ b/src/setuptools_scm/_entrypoints.py
@@ -6,7 +6,6 @@ from typing import Callable
 from typing import cast
 from typing import Iterator
 from typing import overload
-from typing import Protocol
 from typing import TYPE_CHECKING
 
 from . import _log
@@ -17,15 +16,32 @@ if TYPE_CHECKING:
     from ._config import Configuration, ParseFunction
 
 
-log = _log.log.getChild("entrypoints")
+from importlib.metadata import EntryPoint as EntryPoint
+
+
+if sys.version_info[:2] < (3, 10):
+    from importlib.metadata import entry_points as legacy_entry_points
+
+    class EntryPoints:
+        _groupdata: list[EntryPoint]
+
+        def __init__(self, groupdata: list[EntryPoint]) -> None:
+            self._groupdata = groupdata
+
+        def select(self, name: str) -> EntryPoints:
+            return EntryPoints([x for x in self._groupdata if x.name == name])
 
+        def __iter__(self) -> Iterator[EntryPoint]:
+            return iter(self._groupdata)
 
-class EntrypointProtocol(Protocol):
-    name: str
-    value: str
+    def entry_points(group: str) -> EntryPoints:
+        return EntryPoints(legacy_entry_points()[group])
 
-    def load(self) -> Any:
-        pass
+else:
+    from importlib.metadata import entry_points, EntryPoints
+
+
+log = _log.log.getChild("entrypoints")
 
 
 def version_from_entrypoint(
@@ -43,27 +59,11 @@ def version_from_entrypoint(
     return None
 
 
-if sys.version_info[:2] < (3, 10):
-    from importlib_metadata import entry_points
-    from importlib_metadata import EntryPoint
-else:
-    from importlib.metadata import entry_points
-    from importlib.metadata import EntryPoint
-
-
-def iter_entry_points(
-    group: str, name: str | None = None
-) -> Iterator[EntrypointProtocol]:
-    eps = entry_points(group=group)
-    res = (
-        eps
-        if name is None
-        else eps.select(  # type: ignore [no-untyped-call]
-            name=name,
-        )
-    )
+def iter_entry_points(group: str, name: str | None = None) -> Iterator[EntryPoint]:
+    eps: EntryPoints = entry_points(group=group)
+    res = eps if name is None else eps.select(name=name)
 
-    return cast(Iterator[EntrypointProtocol], iter(res))
+    return iter(res)
 
 
 def _get_ep(group: str, name: str) -> Any | None:
@@ -76,7 +76,7 @@ def _get_ep(group: str, name: str) -> Any | None:
 
 def _get_from_object_reference_str(path: str, group: str) -> Any | None:
     # todo: remove for importlib native spelling
-    ep: EntrypointProtocol = EntryPoint(path, path, group)
+    ep = EntryPoint(path, path, group)
     try:
         return ep.load()
     except (AttributeError, ModuleNotFoundError):
diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py
index 1dd280b..36790fa 100644
--- a/src/setuptools_scm/_integration/dump_version.py
+++ b/src/setuptools_scm/_integration/dump_version.py
@@ -15,10 +15,12 @@ TEMPLATES = {
     ".py": """\
 # file generated by setuptools_scm
 # don't change, don't track in version control
-from __future__ import annotations
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+    from typing import Tuple
 
 __version__ = version = {version!r}  # type: str
-__version_tuple__ = version_tuple = {version_tuple!r}  # type: tuple[int | str, ...]
+__version_tuple__ = version_tuple = {version_tuple!r}  # type: Tuple[int | str, ...]
 """,
     ".txt": "{version}",
 }
diff --git a/src/setuptools_scm/discover.py b/src/setuptools_scm/discover.py
index 8bf3899..b12b2f1 100644
--- a/src/setuptools_scm/discover.py
+++ b/src/setuptools_scm/discover.py
@@ -47,7 +47,7 @@ _BLOCKED_EP_TARGETS = {"setuptools_scm_git_archive:parse"}
 
 def iter_matching_entrypoints(
     root: _t.PathT, entrypoint: str, config: Configuration
-) -> Iterable[_entrypoints.EntrypointProtocol]:
+) -> Iterable[_entrypoints.EntryPoint]:
     """
     Consider different entry-points in ``root`` and optionally its parents.
     :param root: File path.


Test patch:
diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py
index 95afa0f..3af099a 100644
--- a/testing/test_basic_api.py
+++ b/testing/test_basic_api.py
@@ -182,7 +182,7 @@ def test_dump_version(tmp_path: Path) -> None:
     assert lines[-2:] == [
         "__version__ = version = '1.0.dev42'  # type: str",
         "__version_tuple__ = version_tuple = (1, 0, 'dev42')"
-        "  # type: tuple[int | str, ...]",
+        "  # type: Tuple[int | str, ...]",
     ]
 
     version = "1.0.1+g4ac9d2c"
diff --git a/testing/test_functions.py b/testing/test_functions.py
index f7a7481..afa45d3 100644
--- a/testing/test_functions.py
+++ b/testing/test_functions.py
@@ -137,7 +137,11 @@ def test_dump_version_mypy(tmp_path: Path) -> None:
     if mypy is None:
         pytest.skip("mypy not found")
     dump_a_version(tmp_path)
-    subprocess.run([mypy, "--strict", "VERSION.py"], cwd=tmp_path, check=True)
+    subprocess.run(
+        [mypy, "--python-version=3.8", "--strict", "VERSION.py"],
+        cwd=tmp_path,
+        check=True,
+    )
 
 
 def test_dump_version_flake8(tmp_path: Path) -> None: