From 118639b8b84dfaf816d062c6bcac8bb2b01eceaf Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 12 Oct 2024 20:29:09 +0200 Subject: [PATCH] ENH: add support for licence and license-files dynamic fields Fixes #270. --- docs/reference/meson-compatibility.rst | 8 +++ mesonpy/__init__.py | 48 +++++++++++++- tests/conftest.py | 7 ++ tests/test_project.py | 90 +++++++++++++++++++++++++- tests/test_sdist.py | 4 +- tests/test_wheel.py | 9 +-- 6 files changed, 153 insertions(+), 13 deletions(-) diff --git a/docs/reference/meson-compatibility.rst b/docs/reference/meson-compatibility.rst index 91c9708bc..24c677c41 100644 --- a/docs/reference/meson-compatibility.rst +++ b/docs/reference/meson-compatibility.rst @@ -39,6 +39,14 @@ versions. Meson 1.3.0 or later is required for compiling extension modules targeting the Python limited API. +.. option:: 1.6.0 + + Meson 1.6.0 or later is required to support ``license`` and + ``license-files`` dynamic fields in ``pyproject.toml`` and to + populate the package license and license files from the ones + declared via the ``project()`` call in ``meson.build``. This also + requires ``pyproject-metadata`` version 0.9.0 or later. + Build front-ends by default build packages in an isolated Python environment where build dependencies are installed. Most often, unless a package or its build dependencies declare explicitly a version diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 1d873d4ef..a72d18504 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -78,6 +78,8 @@ def canonicalize_license_expression(s: str) -> str: __version__ = '0.18.0.dev0' +_PYPROJECT_METADATA_VERSION = tuple(map(int, pyproject_metadata.__version__.split('.')[:2])) +_SUPPORTED_DYNAMIC_FIELDS = {'version', } if _PYPROJECT_METADATA_VERSION < (0, 9) else {'version', 'license', 'license-files'} _NINJA_REQUIRED_VERSION = '1.8.2' _MESON_REQUIRED_VERSION = '0.63.3' # keep in sync with the version requirement in pyproject.toml @@ -260,7 +262,7 @@ def from_pyproject( # type: ignore[override] metadata = super().from_pyproject(data, project_dir, metadata_version) # Check for unsupported dynamic fields. - unsupported_dynamic = set(metadata.dynamic) - {'version', } + unsupported_dynamic = set(metadata.dynamic) - _SUPPORTED_DYNAMIC_FIELDS if unsupported_dynamic: fields = ', '.join(f'"{x}"' for x in unsupported_dynamic) raise pyproject_metadata.ConfigurationError(f'Unsupported dynamic fields: {fields}') @@ -731,13 +733,30 @@ def __init__( raise pyproject_metadata.ConfigurationError( 'Field "version" declared as dynamic but version is not defined in meson.build') self._metadata.version = packaging.version.Version(version) + if 'license' in self._metadata.dynamic: + license = self._meson_license + if license is None: + raise pyproject_metadata.ConfigurationError( + 'Field "license" declared as dynamic but license is not specified in meson.build') + # mypy is not happy when analyzing typing based on + # pyproject-metadata < 0.9 where license needs to be of + # License type. However, this code is not executed if + # pyproject-metadata is older than 0.9 because then dynamic + # license is not allowed. + self._metadata.license = license # type: ignore[assignment] + if 'license-files' in self._metadata.dynamic: + self._metadata.license_files = self._meson_license_files else: # if project section is missing, use minimal metdata from meson.build name, version = self._meson_name, self._meson_version if version is None: raise pyproject_metadata.ConfigurationError( 'Section "project" missing in pyproject.toml and version is not defined in meson.build') - self._metadata = Metadata(name=name, version=packaging.version.Version(version)) + kwargs = { + 'license': self._meson_license, + 'license_files': self._meson_license_files + } if _PYPROJECT_METADATA_VERSION >= (0, 9) else {} + self._metadata = Metadata(name=name, version=packaging.version.Version(version), **kwargs) # verify that we are running on a supported interpreter if self._metadata.requires_python: @@ -862,6 +881,31 @@ def _meson_version(self) -> Optional[str]: return None return value + @property + def _meson_license(self) -> Optional[str]: + """The license specified with the ``license`` argument to ``project()`` in meson.build.""" + value = self._info('intro-projectinfo').get('license', None) + if value is None: + return None + assert isinstance(value, list) + if len(value) > 1: + raise pyproject_metadata.ConfigurationError( + 'using a list of strings for the license declared in meson.build is ambiguous: use a SPDX license expression') + value = value[0] + assert isinstance(value, str) + if value == 'unknown': + return None + return str(canonicalize_license_expression(value)) # str() is to make mypy happy + + @property + def _meson_license_files(self) -> Optional[List[pathlib.Path]]: + """The license files specified with the ``license_files`` argument to ``project()`` in meson.build.""" + value = self._info('intro-projectinfo').get('license_files', None) + if not value: + return None + assert isinstance(value, list) + return [pathlib.Path(x) for x in value] + def sdist(self, directory: Path) -> pathlib.Path: """Generates a sdist (source distribution) in the specified directory.""" # Generate meson dist file. diff --git a/tests/conftest.py b/tests/conftest.py index 0d0941c9f..369f9a31e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ import packaging.metadata import packaging.version +import pyproject_metadata import pytest import mesonpy @@ -25,6 +26,12 @@ from mesonpy._util import chdir +PYPROJECT_METADATA_VERSION = tuple(map(int, pyproject_metadata.__version__.split('.')[:2])) + +_meson_ver_str = subprocess.run(['meson', '--version'], check=True, stdout=subprocess.PIPE, text=True).stdout +MESON_VERSION = tuple(map(int, _meson_ver_str.split('.')[:3])) + + def metadata(data): meta, other = packaging.metadata.parse_email(data) # PEP-639 support requires packaging >= 24.1. Add minimal diff --git a/tests/test_project.py b/tests/test_project.py index d2a4e5e47..15bb76d44 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -19,7 +19,7 @@ import mesonpy -from .conftest import in_git_repo_context, package_dir +from .conftest import MESON_VERSION, PYPROJECT_METADATA_VERSION, in_git_repo_context, metadata, package_dir def test_unsupported_python_version(package_unsupported_python_version): @@ -40,6 +40,94 @@ def test_missing_dynamic_version(package_missing_dynamic_version): pass +@pytest.mark.skipif(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old') +@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old') +@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression') +def test_meson_build_metadata(tmp_path): + tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent(''' + [build-system] + build-backend = 'mesonpy' + requires = ['meson-python'] + '''), encoding='utf8') + + tmp_path.joinpath('meson.build').write_text(textwrap.dedent(''' + project('test', version: '1.2.3', license: 'MIT', license_files: 'LICENSE') + '''), encoding='utf8') + + tmp_path.joinpath('LICENSE').write_text('') + + p = mesonpy.Project(tmp_path, tmp_path / 'build') + + assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\ + Metadata-Version: 2.4 + Name: test + Version: 1.2.3 + License-Expression: MIT + License-File: LICENSE + ''')) + + +@pytest.mark.skipif(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old') +@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old') +@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression') +def test_dynamic_license(tmp_path): + tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent(''' + [build-system] + build-backend = 'mesonpy' + requires = ['meson-python'] + + [project] + name = 'test' + version = '1.0.0' + dynamic = ['license'] + '''), encoding='utf8') + + tmp_path.joinpath('meson.build').write_text(textwrap.dedent(''' + project('test', license: 'MIT') + '''), encoding='utf8') + + p = mesonpy.Project(tmp_path, tmp_path / 'build') + + assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\ + Metadata-Version: 2.4 + Name: test + Version: 1.0.0 + License-Expression: MIT + ''')) + + +@pytest.mark.skipif(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old') +@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old') +@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression') +def test_dynamic_license_files(tmp_path): + tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent(''' + [build-system] + build-backend = 'mesonpy' + requires = ['meson-python'] + + [project] + name = 'test' + version = '1.0.0' + dynamic = ['license', 'license-files'] + '''), encoding='utf8') + + tmp_path.joinpath('meson.build').write_text(textwrap.dedent(''' + project('test', license: 'MIT', license_files: ['LICENSE']) + '''), encoding='utf8') + + tmp_path.joinpath('LICENSE').write_text('') + + p = mesonpy.Project(tmp_path, tmp_path / 'build') + + assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\ + Metadata-Version: 2.4 + Name: test + Version: 1.0.0 + License-Expression: MIT + License-File: LICENSE + ''')) + + def test_user_args(package_user_args, tmp_path, monkeypatch): project_run = mesonpy.Project._run cmds = [] diff --git a/tests/test_sdist.py b/tests/test_sdist.py index 83739d4d0..13dde97ab 100644 --- a/tests/test_sdist.py +++ b/tests/test_sdist.py @@ -17,7 +17,7 @@ from .conftest import in_git_repo_context, metadata -def test_no_pep621(sdist_library): +def test_meson_build_metadata(sdist_library): with tarfile.open(sdist_library, 'r:gz') as sdist: sdist_pkg_info = sdist.extractfile('library-1.0.0/PKG-INFO').read() @@ -28,7 +28,7 @@ def test_no_pep621(sdist_library): ''')) -def test_pep621(sdist_full_metadata): +def test_pep621_metadata(sdist_full_metadata): with tarfile.open(sdist_full_metadata, 'r:gz') as sdist: sdist_pkg_info = sdist.extractfile('full_metadata-1.2.3/PKG-INFO').read() diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 6d29bd636..ceb04843f 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -6,26 +6,19 @@ import re import shutil import stat -import subprocess import sys import sysconfig import textwrap import packaging.tags -import pyproject_metadata import pytest import wheel.wheelfile import mesonpy -from .conftest import adjust_packaging_platform_tag, metadata +from .conftest import MESON_VERSION, PYPROJECT_METADATA_VERSION, adjust_packaging_platform_tag, metadata -PYPROJECT_METADATA_VERSION = tuple(map(int, pyproject_metadata.__version__.split('.')[:2])) - -_meson_ver_str = subprocess.run(['meson', '--version'], check=True, stdout=subprocess.PIPE, text=True).stdout -MESON_VERSION = tuple(map(int, _meson_ver_str.split('.')[:3])) - EXT_SUFFIX = sysconfig.get_config_var('EXT_SUFFIX') if sys.version_info <= (3, 8, 7): if MESON_VERSION >= (0, 99):