Repository: pypa/build (python)

Issue:
InvalidRequirement on Python 3.8
In pypa/setuptools#3965, I discovered that `build` fails to build a dependency with a url req that's part of extras because it stumbles on python/importlib_metadata#357, which was only backported to Python 3.9, but [build uses stdlib importlib.metadata for Python 3.8](https://github.com/pypa/build/blob/cd06da25481b9a610f846fa60cb67b5a5fa9a051/src/build/__init__.py#L150-L153). Build should probably use `importlib_metadata` for Python 3.8 and maybe for Python 3.9 ([quite a few improvements and fixes](https://importlib-metadata.readthedocs.io/en/latest/history.html#v4-3-0) landed in CPython 3.10).

Pull request:
Bump importlib metadata dependency
- Consolidate importlib metadata import
- Rely on importlib_metadata for Python 3.9 and earlier. Fixes #630.


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index acd8eb8..074282d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -35,7 +35,7 @@ repos:
     hooks:
       - id: prettier
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.0.275
+    rev: v0.0.276
     hooks:
       - id: ruff
         args: [--fix, --format, grouped, --show-fixes]
diff --git a/pyproject.toml b/pyproject.toml
index dfeb60a..0457578 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,7 +36,7 @@ dependencies = [
   "pyproject_hooks",
   # not actually a runtime dependency, only supplied as there is not "recommended dependency" support
   'colorama; os_name == "nt"',
-  'importlib-metadata >= 0.22; python_version < "3.8"',
+  'importlib-metadata >= 4.6; python_version < "3.10"',  # Not required for 3.8+, but fixes a stdlib bug
   'tomli >= 1.1.0; python_version < "3.11"',
 ]
 
diff --git a/src/build/_importlib.py b/src/build/_importlib.py
new file mode 100644
index 0000000..f95b2a6
--- /dev/null
+++ b/src/build/_importlib.py
@@ -0,0 +1,14 @@
+import sys
+
+
+if sys.version_info < (3, 8):
+    import importlib_metadata as metadata
+elif sys.version_info < (3, 9, 10) or (3, 10, 0) <= sys.version_info < (3, 10, 2):
+    try:
+        import importlib_metadata as metadata
+    except ModuleNotFoundError:
+        from importlib import metadata
+else:
+    from importlib import metadata
+
+__all__ = ['metadata']
diff --git a/src/build/_util.py b/src/build/_util.py
index 234297f..0582a28 100644
--- a/src/build/_util.py
+++ b/src/build/_util.py
@@ -27,10 +27,7 @@ def check_dependency(
     """
     import packaging.requirements
 
-    if sys.version_info >= (3, 8):
-        import importlib.metadata as importlib_metadata
-    else:
-        import importlib_metadata
+    from ._importlib import metadata
 
     req = packaging.requirements.Requirement(req_string)
     normalised_req_string = str(req)
@@ -51,8 +48,8 @@ def check_dependency(
             return
 
     try:
-        dist = importlib_metadata.distribution(req.name)
-    except importlib_metadata.PackageNotFoundError:
+        dist = metadata.distribution(req.name)
+    except metadata.PackageNotFoundError:
         # dependency is not installed in the environment.
         yield (*ancestral_req_strings, normalised_req_string)
     else:
diff --git a/src/build/util.py b/src/build/util.py
index 9f0570e..9f204b8 100644
--- a/src/build/util.py
+++ b/src/build/util.py
@@ -3,25 +3,19 @@
 from __future__ import annotations
 
 import pathlib
-import sys
 import tempfile
 
 import pyproject_hooks
 
 from . import PathType, ProjectBuilder, RunnerType
+from ._importlib import metadata
 from .env import DefaultIsolatedEnv
 
 
-if sys.version_info >= (3, 8):
-    import importlib.metadata as importlib_metadata
-else:
-    import importlib_metadata
-
-
-def _project_wheel_metadata(builder: ProjectBuilder) -> importlib_metadata.PackageMetadata:
+def _project_wheel_metadata(builder: ProjectBuilder) -> metadata.PackageMetadata:
     with tempfile.TemporaryDirectory() as tmpdir:
         path = pathlib.Path(builder.metadata_path(tmpdir))
-        return importlib_metadata.PathDistribution(path).metadata
+        return metadata.PathDistribution(path).metadata
 
 
 def project_wheel_metadata(
@@ -29,7 +23,7 @@ def project_wheel_metadata(
     isolated: bool = True,
     *,
     runner: RunnerType = pyproject_hooks.quiet_subprocess_runner,
-) -> importlib_metadata.PackageMetadata:
+) -> metadata.PackageMetadata:
     """
     Return the wheel metadata for a project.
 


Test patch:
diff --git a/tests/constraints.txt b/tests/constraints.txt
index ee7ad5f..36446db 100644
--- a/tests/constraints.txt
+++ b/tests/constraints.txt
@@ -1,4 +1,4 @@
-importlib-metadata==0.22
+importlib-metadata==4.6
 packaging==19.0
 pyproject_hooks==1.0
 setuptools==42.0.0; python_version < "3.10"
diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py
index f03839d..98a96ee 100644
--- a/tests/test_projectbuilder.py
+++ b/tests/test_projectbuilder.py
@@ -4,6 +4,7 @@
 import copy
 import logging
 import os
+import pathlib
 import sys
 import textwrap
 
@@ -12,13 +13,7 @@ import pytest
 
 import build
 
-
-if sys.version_info >= (3, 8):  # pragma: no cover
-    from importlib import metadata as importlib_metadata
-else:  # pragma: no cover
-    import importlib_metadata
-
-import pathlib
+from build import _importlib
 
 
 build_open_owner = 'builtins'
@@ -30,7 +25,7 @@ DEFAULT_BACKEND = {
 }
 
 
-class MockDistribution(importlib_metadata.Distribution):
+class MockDistribution(_importlib.metadata.Distribution):
     def locate_file(self, path):  # pragma: no cover
         return ''
 
@@ -48,7 +43,7 @@ class MockDistribution(importlib_metadata.Distribution):
             return CircularMockDistribution()
         elif name == 'nested_circular_dep':
             return NestedCircularMockDistribution()
-        raise importlib_metadata.PackageNotFoundError
+        raise _importlib.metadata.PackageNotFoundError
 
 
 class ExtraMockDistribution(MockDistribution):
@@ -167,7 +162,7 @@ class NestedCircularMockDistribution(MockDistribution):
     ],
 )
 def test_check_dependency(monkeypatch, requirement_string, expected):
-    monkeypatch.setattr(importlib_metadata, 'Distribution', MockDistribution)
+    monkeypatch.setattr(_importlib.metadata, 'Distribution', MockDistribution)
     assert next(build.check_dependency(requirement_string), None) == expected
 
 
@@ -502,7 +497,7 @@ def test_runner_user_specified(tmp_dir, package_test_flit):
 def test_metadata_path_no_prepare(tmp_dir, package_test_no_prepare):
     builder = build.ProjectBuilder(package_test_no_prepare)
 
-    metadata = importlib_metadata.PathDistribution(
+    metadata = _importlib.metadata.PathDistribution(
         pathlib.Path(builder.metadata_path(tmp_dir)),
     ).metadata
 
@@ -513,7 +508,7 @@ def test_metadata_path_no_prepare(tmp_dir, package_test_no_prepare):
 def test_metadata_path_with_prepare(tmp_dir, package_test_setuptools):
     builder = build.ProjectBuilder(package_test_setuptools)
 
-    metadata = importlib_metadata.PathDistribution(
+    metadata = _importlib.metadata.PathDistribution(
         pathlib.Path(builder.metadata_path(tmp_dir)),
     ).metadata
 
@@ -524,7 +519,7 @@ def test_metadata_path_with_prepare(tmp_dir, package_test_setuptools):
 def test_metadata_path_legacy(tmp_dir, package_legacy):
     builder = build.ProjectBuilder(package_legacy)
 
-    metadata = importlib_metadata.PathDistribution(
+    metadata = _importlib.metadata.PathDistribution(
         pathlib.Path(builder.metadata_path(tmp_dir)),
     ).metadata
 
diff --git a/tests/test_self_packaging.py b/tests/test_self_packaging.py
index 00870fd..b134f53 100644
--- a/tests/test_self_packaging.py
+++ b/tests/test_self_packaging.py
@@ -21,6 +21,7 @@ sdist_files = {
     'src/build/__init__.py',
     'src/build/__main__.py',
     'src/build/_exceptions.py',
+    'src/build/_importlib.py',
     'src/build/_util.py',
     'src/build/env.py',
     'src/build/py.typed',
@@ -31,6 +32,7 @@ wheel_files = {
     'build/__init__.py',
     'build/__main__.py',
     'build/_exceptions.py',
+    'build/_importlib.py',
     'build/_util.py',
     'build/env.py',
     'build/py.typed',