Repository: pypa/build
Pull request #742 (2024-02-26 20:55:23)

compat: create separate importlib, tarfile and tomllib shims

henryiii: Looks good. Did we want to pull out tomllib here too, or in a follow-up, etc?

Patch:
diff --git a/src/build/__init__.py b/src/build/__init__.py
index 8b3b268..c92daba 100644
--- a/src/build/__init__.py
+++ b/src/build/__init__.py
@@ -24,6 +24,7 @@ from typing import Any, Callable, Mapping, Optional, Sequence, TypeVar, Union
 import pyproject_hooks
 
 from . import env
+from ._compat import tomllib
 from ._exceptions import (
     BuildBackendException,
     BuildException,
@@ -34,12 +35,6 @@ from ._exceptions import (
 from ._util import check_dependency, parse_wheel_filename
 
 
-if sys.version_info >= (3, 11):
-    import tomllib
-else:
-    import tomli as tomllib
-
-
 RunnerType = Callable[[Sequence[str], Optional[str], Optional[Mapping[str, str]]], None]
 ConfigSettingsType = Mapping[str, Union[str, Sequence[str]]]
 PathType = Union[str, 'os.PathLike[str]']
diff --git a/src/build/__main__.py b/src/build/__main__.py
index 985caf9..03c3dcd 100644
--- a/src/build/__main__.py
+++ b/src/build/__main__.py
@@ -230,7 +230,7 @@ def build_package_via_sdist(
     :param isolation: Isolate the build in a separate environment
     :param skip_dependency_check: Do not perform the dependency check
     """
-    from ._util import TarFile
+    from ._compat import tarfile
 
     if 'sdist' in distributions:
         msg = 'Only binary distributions are allowed but sdist was specified'
@@ -243,7 +243,7 @@ def build_package_via_sdist(
     built: list[str] = []
     if distributions:
         # extract sdist
-        with TarFile.open(sdist) as t:
+        with tarfile.TarFile.open(sdist) as t:
             t.extractall(sdist_out)
             try:
                 _ProjectBuilder.log(f'Building {_natural_language_list(distributions)} from sdist')
diff --git a/src/build/_compat/__init__.py b/src/build/_compat/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/build/_importlib.py b/src/build/_compat/importlib.py
similarity index 84%
rename from src/build/_importlib.py
rename to src/build/_compat/importlib.py
index 0282729..7487ddf 100644
--- a/src/build/_importlib.py
+++ b/src/build/_compat/importlib.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import sys
 
 
@@ -12,4 +14,7 @@ else:
         # helps bootstrapping when dependencies aren't installed
         from importlib import metadata
 
-__all__ = ['metadata']
+
+__all__ = [
+    'metadata',
+]
diff --git a/src/build/_compat/tarfile.py b/src/build/_compat/tarfile.py
new file mode 100644
index 0000000..2984a49
--- /dev/null
+++ b/src/build/_compat/tarfile.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+import sys
+import tarfile
+import typing
+
+
+if typing.TYPE_CHECKING:
+    TarFile = tarfile.TarFile
+
+else:
+    # Per https://peps.python.org/pep-0706/, the "data" filter will become
+    # the default in Python 3.14. The first series of releases with the filter
+    # had a broken filter that could not process symlinks correctly.
+    if (
+        (3, 8, 18) <= sys.version_info < (3, 9)
+        or (3, 9, 18) <= sys.version_info < (3, 10)
+        or (3, 10, 13) <= sys.version_info < (3, 11)
+        or (3, 11, 5) <= sys.version_info < (3, 12)
+        or (3, 12) <= sys.version_info < (3, 14)
+    ):
+
+        class TarFile(tarfile.TarFile):
+            extraction_filter = staticmethod(tarfile.data_filter)
+
+    else:
+        TarFile = tarfile.TarFile
+
+
+__all__ = [
+    'TarFile',
+]
diff --git a/src/build/_compat/tomllib.py b/src/build/_compat/tomllib.py
new file mode 100644
index 0000000..51191ef
--- /dev/null
+++ b/src/build/_compat/tomllib.py
@@ -0,0 +1,16 @@
+from __future__ import annotations
+
+import sys
+
+
+if sys.version_info >= (3, 11):
+    from tomllib import TOMLDecodeError, load, loads
+else:
+    from tomli import TOMLDecodeError, load, loads
+
+
+__all__ = [
+    'TOMLDecodeError',
+    'load',
+    'loads',
+]
diff --git a/src/build/_util.py b/src/build/_util.py
index 691e15a..a57c41e 100644
--- a/src/build/_util.py
+++ b/src/build/_util.py
@@ -1,9 +1,6 @@
 from __future__ import annotations
 
 import re
-import sys
-import tarfile
-import typing
 
 from collections.abc import Iterator, Set
 
@@ -27,7 +24,7 @@ def check_dependency(
     """
     import packaging.requirements
 
-    from ._importlib import metadata
+    from ._compat import importlib
 
     req = packaging.requirements.Requirement(req_string)
     normalised_req_string = str(req)
@@ -48,8 +45,8 @@ def check_dependency(
             return
 
     try:
-        dist = metadata.distribution(req.name)
-    except metadata.PackageNotFoundError:
+        dist = importlib.metadata.distribution(req.name)
+    except importlib.metadata.PackageNotFoundError:
         # dependency is not installed in the environment.
         yield (*ancestral_req_strings, normalised_req_string)
     else:
@@ -64,25 +61,3 @@ def check_dependency(
 
 def parse_wheel_filename(filename: str) -> re.Match[str] | None:
     return _WHEEL_FILENAME_REGEX.match(filename)
-
-
-if typing.TYPE_CHECKING:
-    TarFile = tarfile.TarFile
-
-else:
-    # Per https://peps.python.org/pep-0706/, the "data" filter will become
-    # the default in Python 3.14. The first series of releases with the filter
-    # had a broken filter that could not process symlinks correctly.
-    if (
-        (3, 8, 18) <= sys.version_info < (3, 9)
-        or (3, 9, 18) <= sys.version_info < (3, 10)
-        or (3, 10, 13) <= sys.version_info < (3, 11)
-        or (3, 11, 5) <= sys.version_info < (3, 12)
-        or (3, 12) <= sys.version_info < (3, 14)
-    ):
-
-        class TarFile(tarfile.TarFile):
-            extraction_filter = staticmethod(tarfile.data_filter)
-
-    else:
-        TarFile = tarfile.TarFile
diff --git a/src/build/env.py b/src/build/env.py
index 95ba9d1..5cb7a3a 100644
--- a/src/build/env.py
+++ b/src/build/env.py
@@ -79,12 +79,12 @@ def _has_valid_pip(**distargs: object) -> bool:
 
     import packaging.version
 
-    from ._importlib import metadata
+    from ._compat import importlib
 
     name = 'pip'
 
     try:
-        pip_distribution = next(iter(metadata.distributions(name=name, **distargs)))
+        pip_distribution = next(iter(importlib.metadata.distributions(name=name, **distargs)))
     except StopIteration:
         raise ModuleNotFoundError(name) from None
 
diff --git a/src/build/util.py b/src/build/util.py
index 9f204b8..671c1c6 100644
--- a/src/build/util.py
+++ b/src/build/util.py
@@ -8,14 +8,14 @@ import tempfile
 import pyproject_hooks
 
 from . import PathType, ProjectBuilder, RunnerType
-from ._importlib import metadata
+from ._compat import importlib
 from .env import DefaultIsolatedEnv
 
 
-def _project_wheel_metadata(builder: ProjectBuilder) -> metadata.PackageMetadata:
+def _project_wheel_metadata(builder: ProjectBuilder) -> importlib.metadata.PackageMetadata:
     with tempfile.TemporaryDirectory() as tmpdir:
         path = pathlib.Path(builder.metadata_path(tmpdir))
-        return metadata.PathDistribution(path).metadata
+        return importlib.metadata.PathDistribution(path).metadata
 
 
 def project_wheel_metadata(
@@ -23,7 +23,7 @@ def project_wheel_metadata(
     isolated: bool = True,
     *,
     runner: RunnerType = pyproject_hooks.quiet_subprocess_runner,
-) -> metadata.PackageMetadata:
+) -> importlib.metadata.PackageMetadata:
     """
     Return the wheel metadata for a project.
 


Test patch:
diff --git a/tests/test_env.py b/tests/test_env.py
index feaf042..379d8e5 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -140,7 +140,7 @@ def test_pip_needs_upgrade_mac_os_11(mocker, pip_version, arch):
     mocker.patch('platform.system', return_value='Darwin')
     mocker.patch('platform.machine', return_value=arch)
     mocker.patch('platform.mac_ver', return_value=('11.0', ('', '', ''), ''))
-    mocker.patch('build._importlib.metadata.distributions', return_value=(SimpleNamespace(version=pip_version),))
+    mocker.patch('build._compat.importlib.metadata.distributions', return_value=(SimpleNamespace(version=pip_version),))
 
     min_version = Version('20.3' if arch == 'x86_64' else '21.0.1')
     with build.env.DefaultIsolatedEnv():
diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py
index 602ca06..cccb385 100644
--- a/tests/test_projectbuilder.py
+++ b/tests/test_projectbuilder.py
@@ -13,7 +13,7 @@ import pytest
 
 import build
 
-from build import _importlib
+from build._compat import importlib as _importlib
 
 
 build_open_owner = 'builtins'
diff --git a/tests/test_self_packaging.py b/tests/test_self_packaging.py
index 3a436ef..9d515e5 100644
--- a/tests/test_self_packaging.py
+++ b/tests/test_self_packaging.py
@@ -34,6 +34,7 @@ sdist_files = {
 sdist_patterns = {
     'docs/*.rst',
     'src/build/*.py',
+    'src/build/_compat/*.py',
     'tests/*.py',
     'tests/packages/*/*.py',
     'tests/packages/*/*/*.py',
@@ -46,8 +47,11 @@ sdist_files |= {str(PurePosixPath(p.relative_to(MAIN_DIR))) for path in sdist_pa
 wheel_files = {
     'build/__init__.py',
     'build/__main__.py',
+    'build/_compat/__init__.py',
+    'build/_compat/importlib.py',
+    'build/_compat/tarfile.py',
+    'build/_compat/tomllib.py',
     'build/_exceptions.py',
-    'build/_importlib.py',
     'build/_util.py',
     'build/env.py',
     'build/py.typed',