Repository: pypa/build
Pull request #340 (2021-08-05 13:22:56)

util: add project_wheel_metadata
Signed-off-by: Filipe Laíns <lains@riseup.net>

layday: There's a new version of `importlib-metadata` out if you have time to revisit this PR @FFY00.
FFY00: Does anyone have other comments?
layday: LGTM!

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c3519aa..c5fa625 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,6 +3,14 @@ Changelog
 +++++++++
 
 
+Unreleased
+==========
+
+- Add ``build.util`` module with an high-level utility API (`PR #340`_)
+
+.. _PR #340: https://github.com/pypa/build/pull/340
+
+
 0.6.0.post1 (05-08-2021)
 ========================
 
diff --git a/setup.cfg b/setup.cfg
index 21aad6b..c8972d8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -60,6 +60,7 @@ test =
     toml>=0.10.0
     wheel>=0.36.0
 typing =
+    importlib-metadata>=4.6.4
     mypy==0.910
     typing-extensions>=3.7.4.3
 virtualenv =
diff --git a/src/build/__init__.py b/src/build/__init__.py
index 42b5c8c..2af5846 100644
--- a/src/build/__init__.py
+++ b/src/build/__init__.py
@@ -138,7 +138,7 @@ def check_dependency(
             return
 
     try:
-        dist = importlib_metadata.distribution(req.name)
+        dist = importlib_metadata.distribution(req.name)  # type: ignore[no-untyped-call]
     except importlib_metadata.PackageNotFoundError:
         # dependency is not installed in the environment.
         yield ancestral_req_strings + (req_string,)
diff --git a/src/build/env.py b/src/build/env.py
index d9d8077..4b0725a 100644
--- a/src/build/env.py
+++ b/src/build/env.py
@@ -258,7 +258,7 @@ def _create_isolated_env_venv(path: str) -> Tuple[str, str]:
     executable, script_dir, purelib = _find_executable_and_scripts(path)
 
     # Get the version of pip in the environment
-    pip_distribution = next(iter(metadata.distributions(name='pip', path=[purelib])))
+    pip_distribution = next(iter(metadata.distributions(name='pip', path=[purelib])))  # type: ignore[no-untyped-call]
     current_pip_version = packaging.version.Version(pip_distribution.version)
 
     if platform.system() == 'Darwin' and int(platform.mac_ver()[0].split('.')[0]) >= 11:
diff --git a/src/build/util.py b/src/build/util.py
new file mode 100644
index 0000000..1710e1c
--- /dev/null
+++ b/src/build/util.py
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: MIT
+
+import os
+import pathlib
+import sys
+import tempfile
+
+from typing import Union
+
+import pep517
+
+import build
+import build.env
+
+
+if sys.version_info >= (3, 8):
+    import importlib.metadata as importlib_metadata
+else:
+    import importlib_metadata
+
+
+def _project_wheel_metadata(builder: build.ProjectBuilder) -> 'importlib_metadata.PackageMetadata':
+    with tempfile.TemporaryDirectory() as tmpdir:
+        path = pathlib.Path(builder.metadata_path(tmpdir))
+        # https://github.com/python/importlib_metadata/pull/343
+        return importlib_metadata.PathDistribution(path).metadata  # type: ignore
+
+
+def project_wheel_metadata(
+    srcdir: Union[str, 'os.PathLike[str]'],
+    isolated: bool = True,
+) -> 'importlib_metadata.PackageMetadata':
+    """
+    Return the wheel metadata for a project.
+
+    Uses the ``prepare_metadata_for_build_wheel`` hook if availablable,
+    otherwise ``build_wheel``.
+
+    :param srcdir: Project source directory
+    :param isolated: Wether or not to run invoke the backend in the current
+                     environment or to create an isolated one and invoke it
+                     there.
+    """
+    builder = build.ProjectBuilder(
+        os.fspath(srcdir),
+        runner=pep517.quiet_subprocess_runner,
+    )
+
+    if not isolated:
+        return _project_wheel_metadata(builder)
+
+    with build.env.IsolatedEnvBuilder() as env:
+        builder.python_executable = env.executable
+        builder.scripts_dir = env.scripts_dir
+        env.install(builder.build_system_requires)
+        env.install(builder.get_requires_for_build('wheel'))
+        return _project_wheel_metadata(builder)
+
+
+__all__ = ('project_wheel_metadata',)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index ee5f08e..357dde3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -173,6 +173,11 @@ def test_invalid_requirements_path(packages_path):
     return os.path.join(packages_path, 'test-invalid-requirements')
 
 
+@pytest.fixture
+def test_metadata(packages_path):
+    return os.path.join(packages_path, 'test-metadata')
+
+
 @pytest.fixture
 def test_no_permission(packages_path):
     path = os.path.join(packages_path, 'test-no-permission')
diff --git a/tests/packages/test-metadata/backend.py b/tests/packages/test-metadata/backend.py
new file mode 100644
index 0000000..727dab9
--- /dev/null
+++ b/tests/packages/test-metadata/backend.py
@@ -0,0 +1,43 @@
+# SPDX-License-Identifier: MIT
+
+import pathlib
+import textwrap
+
+
+def get_requires_for_build_wheel(config_settings=None):
+    return ['tomli']
+
+
+def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
+    import tomli
+
+    with open('pyproject.toml', 'rb') as f:
+        metadata = tomli.load(f)
+
+    distinfo = pathlib.Path(
+        metadata_directory,
+        '{}-{}.dist-info'.format(
+            metadata['project']['name'].replace('-', '-'),
+            metadata['project']['version'],
+        ),
+    )
+    distinfo.mkdir(parents=True, exist_ok=True)
+    distinfo.joinpath('METADATA').write_text(
+        textwrap.dedent(
+            f'''
+            Metadata-Version: 2.2
+            Name: {metadata['project']['name']}
+            Version: {metadata['project']['version']}
+            Summary: {metadata['project']['description']}
+            '''
+        ).strip()
+    )
+    return distinfo.name
+
+
+def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
+    raise NotImplementedError
+
+
+def build_sdist(sdist_directory, config_settings=None):
+    raise NotImplementedError
diff --git a/tests/packages/test-metadata/pyproject.toml b/tests/packages/test-metadata/pyproject.toml
new file mode 100644
index 0000000..3900263
--- /dev/null
+++ b/tests/packages/test-metadata/pyproject.toml
@@ -0,0 +1,14 @@
+[build-system]
+requires = []
+build-backend = 'backend'
+backend-path = ['.']
+
+[project]
+name = 'test-metadata'
+version = '1.0.0'
+description = 'hello!'
+
+[tool.black]
+line-length = 127
+skip-string-normalization = true
+target-version = ['py39', 'py38', 'py37', 'py36']
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 0000000..08c81ef
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: MIT
+
+import pytest
+
+import build.util
+
+
+@pytest.mark.parametrize('isolated', [False, True])
+def test_wheel_metadata(test_setuptools_path, isolated):
+    metadata = build.util.project_wheel_metadata(test_setuptools_path, isolated)
+
+    assert metadata['name'] == 'test-setuptools'
+    assert metadata['version'] == '1.0.0'
+
+
+def test_wheel_metadata_isolation(test_flit_path):
+    metadata = build.util.project_wheel_metadata(test_flit_path)
+
+    assert metadata['name'] == 'test_flit'
+    assert metadata['version'] == '1.0.0'
+
+    with pytest.raises(
+        build.BuildBackendException,
+        match="Backend 'flit_core.buildapi' is not available.",
+    ):
+        build.util.project_wheel_metadata(test_flit_path, isolated=False)
+
+
+def test_with_get_requires(test_metadata):
+    metadata = build.util.project_wheel_metadata(test_metadata)
+
+    assert metadata['name'] == 'test-metadata'
+    assert str(metadata['version']) == '1.0.0'
+    assert metadata['summary'] == 'hello!'