From 0e013f3bd31c2ae3a2ade7f0611a6e6891d7e919 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 12 Oct 2024 11:27:45 +0200 Subject: [PATCH 1/5] ENH: add support for PEP 639 --- mesonpy/__init__.py | 41 ++++++++++++++----- tests/conftest.py | 11 +++++ .../license-pep639/LICENSES/BSD-3-Clause.txt | 1 + .../packages/license-pep639/LICENSES/MIT.txt | 1 + tests/packages/license-pep639/meson.build | 5 +++ tests/packages/license-pep639/pyproject.toml | 13 ++++++ tests/test_metadata.py | 28 +++++++++++++ tests/test_wheel.py | 28 ++++++++++++- 8 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 tests/packages/license-pep639/LICENSES/BSD-3-Clause.txt create mode 100644 tests/packages/license-pep639/LICENSES/MIT.txt create mode 100644 tests/packages/license-pep639/meson.build create mode 100644 tests/packages/license-pep639/pyproject.toml diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 6f982c71..1acc639b 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -53,6 +53,20 @@ from mesonpy._compat import cached_property, read_binary +try: + from packaging.licenses import InvalidLicenseExpression, canonicalize_license_expression +except ImportError: + # PEP-639 support requires packaging >= 24.2. + def canonicalize_license_expression(s: str) -> str: # type: ignore[misc] + warnings.warn( + 'canonicalization and validation of license expression in "project.license" ' + 'as defined by PEP-639 requires packaging version 24.2 or later.', stacklevel=2) + return s + + class InvalidLicenseExpression(Exception): # type: ignore[no-redef] + pass + + if typing.TYPE_CHECKING: # pragma: no cover from typing import Any, Callable, DefaultDict, Dict, List, Literal, Optional, Sequence, TextIO, Tuple, Type, TypeVar, Union @@ -251,6 +265,10 @@ def from_pyproject( # type: ignore[override] fields = ', '.join(f'"{x}"' for x in unsupported_dynamic) raise pyproject_metadata.ConfigurationError(f'Unsupported dynamic fields: {fields}') + # Validate license field to be a valid SDPX license expression. + if isinstance(metadata.license, str): + metadata.license = canonicalize_license_expression(metadata.license) + return metadata @property @@ -339,13 +357,6 @@ def _data_dir(self) -> str: def _libs_dir(self) -> str: return f'.{self._metadata.distribution_name}.mesonpy.libs' - @property - def _license_file(self) -> Optional[pathlib.Path]: - license_ = self._metadata.license - if license_ and isinstance(license_, pyproject_metadata.License): - return license_.file - return None - @property def wheel(self) -> bytes: """Return WHEEL file for dist-info.""" @@ -428,9 +439,17 @@ def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None: if self.entrypoints_txt: whl.writestr(f'{self._distinfo_dir}/entry_points.txt', self.entrypoints_txt) - # add license (see https://github.com/mesonbuild/meson-python/issues/88) - if self._license_file: - whl.write(self._license_file, f'{self._distinfo_dir}/{os.path.basename(self._license_file)}') + # Add pre-PEP-639 license files. + if isinstance(self._metadata.license, pyproject_metadata.License): + license_file = self._metadata.license.file + if license_file: + whl.write(license_file, f'{self._distinfo_dir}/{os.path.basename(license_file)}') + + # Add PEP-639 license-files. Use ``getattr()`` for compatibility with pyproject-metadata < 0.9.0. + license_files = getattr(self._metadata, 'license_files', None) + if license_files: + for f in license_files: + whl.write(f, f'{self._distinfo_dir}/licenses/{pathlib.Path(f).as_posix()}') def build(self, directory: Path) -> pathlib.Path: wheel_file = pathlib.Path(directory, f'{self.name}.whl') @@ -1023,7 +1042,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: warnings.showwarning = _showwarning try: return func(*args, **kwargs) - except (Error, pyproject_metadata.ConfigurationError) as exc: + except (Error, InvalidLicenseExpression, pyproject_metadata.ConfigurationError) as exc: prefix = f'{style.ERROR}meson-python: error:{style.RESET} ' _log('\n' + textwrap.indent(str(exc), prefix)) raise SystemExit(1) from exc diff --git a/tests/conftest.py b/tests/conftest.py index 1280a5d3..8aedfada 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,6 +28,17 @@ def metadata(data): meta, other = packaging.metadata.parse_email(data) + # PEP-639 support requires packaging >= 24.1. Add minimal + # handling of PEP-639 fields here to allow testing with older + # packaging releases. + value = other.pop('license-expression', None) + if value is not None: + # The ``License-Expression`` header should appear only once. + assert len(value) == 1 + meta['license-expression'] = value[0] + value = other.pop('license-file', None) + if value is not None: + meta['license-file'] = value assert not other return meta diff --git a/tests/packages/license-pep639/LICENSES/BSD-3-Clause.txt b/tests/packages/license-pep639/LICENSES/BSD-3-Clause.txt new file mode 100644 index 00000000..ce9b60c2 --- /dev/null +++ b/tests/packages/license-pep639/LICENSES/BSD-3-Clause.txt @@ -0,0 +1 @@ +Placeholder, just for testing. diff --git a/tests/packages/license-pep639/LICENSES/MIT.txt b/tests/packages/license-pep639/LICENSES/MIT.txt new file mode 100644 index 00000000..ce9b60c2 --- /dev/null +++ b/tests/packages/license-pep639/LICENSES/MIT.txt @@ -0,0 +1 @@ +Placeholder, just for testing. diff --git a/tests/packages/license-pep639/meson.build b/tests/packages/license-pep639/meson.build new file mode 100644 index 00000000..495a5563 --- /dev/null +++ b/tests/packages/license-pep639/meson.build @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2022 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('license-pep639', version: '1.0.0') diff --git a/tests/packages/license-pep639/pyproject.toml b/tests/packages/license-pep639/pyproject.toml new file mode 100644 index 00000000..97416f3a --- /dev/null +++ b/tests/packages/license-pep639/pyproject.toml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2022 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] + +[project] +name = 'license-pep639' +version = '1.0.0' +license = 'MIT OR BSD-3-Clause' +license-files = ['LICENSES/*'] diff --git a/tests/test_metadata.py b/tests/test_metadata.py index d219b90c..354ae012 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -12,6 +12,12 @@ from mesonpy import Metadata +try: + import packaging.licenses as packaging_licenses +except ImportError: + packaging_licenses = None + + def test_package_name(): name = 'package.Test' metadata = Metadata(name='package.Test', version=packaging.version.Version('0.0.1')) @@ -57,3 +63,25 @@ def test_missing_version(package_missing_version): )) with pytest.raises(pyproject_metadata.ConfigurationError, match=match): Metadata.from_pyproject(pyproject, pathlib.Path()) + + +@pytest.mark.skipif(packaging_licenses is None, reason='packaging too old') +def test_normalize_license(): + pyproject = {'project': { + 'name': 'test', + 'version': '1.2.3', + 'license': 'mit or bsd-3-clause', + }} + metadata = Metadata.from_pyproject(pyproject, pathlib.Path()) + assert metadata.license == 'MIT OR BSD-3-Clause' + + +@pytest.mark.skipif(packaging_licenses is None, reason='packaging too old') +def test_invalid_license(): + pyproject = {'project': { + 'name': 'test', + 'version': '1.2.3', + 'license': 'Foo', + }} + with pytest.raises(packaging_licenses.InvalidLicenseExpression, match='Unknown license: \'foo\''): + Metadata.from_pyproject(pyproject, pathlib.Path()) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 1b819823..2ccd1587 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -12,14 +12,17 @@ import textwrap import packaging.tags +import pyproject_metadata import pytest import wheel.wheelfile import mesonpy -from .conftest import adjust_packaging_platform_tag +from .conftest import 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])) @@ -140,6 +143,29 @@ def test_contents_license_file(wheel_license_file): assert artifact.read('license_file-1.0.0.dist-info/LICENSE.custom').rstrip() == b'Hello!' +@pytest.mark.xfail(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old') +@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression') +def test_license_pep639(wheel_license_pep639): + artifact = wheel.wheelfile.WheelFile(wheel_license_pep639) + + assert wheel_contents(artifact) == { + 'license_pep639-1.0.0.dist-info/METADATA', + 'license_pep639-1.0.0.dist-info/RECORD', + 'license_pep639-1.0.0.dist-info/WHEEL', + 'license_pep639-1.0.0.dist-info/licenses/LICENSES/BSD-3-Clause.txt', + 'license_pep639-1.0.0.dist-info/licenses/LICENSES/MIT.txt', + } + + assert metadata(artifact.read('license_pep639-1.0.0.dist-info/METADATA')) == metadata(textwrap.dedent('''\ + Metadata-Version: 2.4 + Name: license-pep639 + Version: 1.0.0 + License-Expression: MIT OR BSD-3-Clause + License-File: LICENSES/BSD-3-Clause.txt + License-File: LICENSES/MIT.txt + ''')) + + @pytest.mark.skipif(sys.platform not in {'linux', 'darwin', 'sunos5'}, reason='Not supported on this platform') def test_contents(package_library, wheel_library): artifact = wheel.wheelfile.WheelFile(wheel_library) From 4b434e5bd86d3d4fe6a93f442bfc3711115955be Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 12 Oct 2024 16:47:33 +0200 Subject: [PATCH 2/5] MAINT: refactor validation of meson introspection data --- mesonpy/__init__.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 1acc639b..c66e91fb 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -727,14 +727,14 @@ def __init__( # set version from meson.build if version is declared as dynamic if 'version' in self._metadata.dynamic: version = self._meson_version - if version == 'undefined': + if version is None: raise pyproject_metadata.ConfigurationError( 'Field "version" declared as dynamic but version is not defined in meson.build') self._metadata.version = packaging.version.Version(version) else: # if project section is missing, use minimal metdata from meson.build name, version = self._meson_name, self._meson_version - if version == 'undefined': + if not version: 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)) @@ -848,17 +848,19 @@ def _manifest(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]: @property def _meson_name(self) -> str: - """Name in meson.build.""" - name = self._info('intro-projectinfo')['descriptive_name'] - assert isinstance(name, str) - return name + """The project name specified with ``project()`` in meson.build.""" + value = self._info('intro-projectinfo')['descriptive_name'] + assert isinstance(value, str) + return value @property - def _meson_version(self) -> str: - """Version in meson.build.""" - name = self._info('intro-projectinfo')['version'] - assert isinstance(name, str) - return name + def _meson_version(self) -> Optional[str]: + """The version specified with the ``version`` argument to ``project()`` in meson.build.""" + value = self._info('intro-projectinfo')['version'] + assert isinstance(value, str) + if value == 'undefined': + return None + return value def sdist(self, directory: Path) -> pathlib.Path: """Generates a sdist (source distribution) in the specified directory.""" @@ -866,7 +868,8 @@ def sdist(self, directory: Path) -> pathlib.Path: self._run(self._meson + ['dist', '--allow-dirty', '--no-tests', '--formats', 'gztar', *self._meson_args['dist']]) dist_name = f'{self._metadata.distribution_name}-{self._metadata.version}' - meson_dist_name = f'{self._meson_name}-{self._meson_version}' + meson_version = self._meson_version or 'undefined' + meson_dist_name = f'{self._meson_name}-{meson_version}' meson_dist_path = pathlib.Path(self._build_dir, 'meson-dist', f'{meson_dist_name}.tar.gz') sdist_path = pathlib.Path(directory, f'{dist_name}.tar.gz') pyproject_toml_mtime = 0 From 8771ca050c8e3796dd66d1de3ea1f8cf782958ca Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 12 Oct 2024 20:29:09 +0200 Subject: [PATCH 3/5] ENH: add support for licence and license-files dynamic fields Fixes #270. --- docs/reference/meson-compatibility.rst | 8 ++ mesonpy/__init__.py | 49 ++++++++- tests/conftest.py | 7 ++ tests/test_project.py | 134 ++++++++++++++++++++++++- tests/test_sdist.py | 4 +- tests/test_wheel.py | 9 +- 6 files changed, 198 insertions(+), 13 deletions(-) diff --git a/docs/reference/meson-compatibility.rst b/docs/reference/meson-compatibility.rst index 743dde4a..6b339d60 100644 --- a/docs/reference/meson-compatibility.rst +++ b/docs/reference/meson-compatibility.rst @@ -45,6 +45,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 c66e91fb..103a8740 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -79,6 +79,9 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef] MesonArgs = Mapping[MesonArgsKeys, List[str]] +_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 +263,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 # type: ignore[operator] if unsupported_dynamic: fields = ', '.join(f'"{x}"' for x in unsupported_dynamic) raise pyproject_metadata.ConfigurationError(f'Unsupported dynamic fields: {fields}') @@ -731,13 +734,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, unused-ignore] + 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 not version: 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 +882,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 8aedfada..7a613bb2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ import packaging.metadata import packaging.version +import pyproject_metadata import pytest import mesonpy @@ -26,6 +27,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 d2a4e5e4..1e95e32f 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,138 @@ 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_list(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', 'BSD-3-Clause']) + '''), encoding='utf8') + + with pytest.raises(pyproject_metadata.ConfigurationError, match='Using a list of strings for the license'): + mesonpy.Project(tmp_path, tmp_path / 'build') + + +@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_missing(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') + '''), encoding='utf8') + + with pytest.raises(pyproject_metadata.ConfigurationError, match='Field "license" declared as dynamic but'): + mesonpy.Project(tmp_path, tmp_path / 'build') + + +@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 ac56bcf0..a155059f 100644 --- a/tests/test_sdist.py +++ b/tests/test_sdist.py @@ -16,7 +16,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() @@ -27,7 +27,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 2ccd1587..b744578c 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): From 87a5afd64294af4637e836a4aacd2d2360d83307 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Mon, 28 Oct 2024 21:39:54 +0100 Subject: [PATCH 4/5] MAINT: bump required pyproject-metadata version to 0.9.0 Having PEP 639 metadata supported or not based on the version of a transitive dependency would not make for a great user experience. Supporting PEP 639 metadata requires pyproject-metadata 0.9.0. --- .github/workflows/tests.yml | 4 ---- pyproject.toml | 4 ++-- tests/conftest.py | 3 --- tests/test_project.py | 5 +---- tests/test_sdist.py | 8 +------- tests/test_wheel.py | 3 +-- 6 files changed, 5 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0bfca7e2..cc48c21c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -98,10 +98,6 @@ jobs: - os: windows-latest python: '3.12' meson: '@git+https://github.com/mesonbuild/meson.git' - # Test with oldest supported pyproject-metadata - - os: ubuntu-latest - python: '3.12' - pyproject_metadata: '==0.8.0' steps: - name: Checkout diff --git a/pyproject.toml b/pyproject.toml index 2247a46d..a64a26d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ requires = [ 'meson >= 0.64.0; python_version < "3.12"', 'meson >= 1.2.3; python_version >= "3.12"', 'packaging >= 19.0', - 'pyproject-metadata >= 0.8.0', + 'pyproject-metadata >= 0.9.0', 'tomli >= 1.0.0; python_version < "3.11"', ] @@ -37,7 +37,7 @@ dependencies = [ 'meson >= 0.64.0; python_version < "3.12"', 'meson >= 1.2.3; python_version >= "3.12"', 'packaging >= 19.0', - 'pyproject-metadata >= 0.8.0', + 'pyproject-metadata >= 0.9.0', 'tomli >= 1.0.0; python_version < "3.11"', ] diff --git a/tests/conftest.py b/tests/conftest.py index 7a613bb2..b53161d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,6 @@ import packaging.metadata import packaging.version -import pyproject_metadata import pytest import mesonpy @@ -27,8 +26,6 @@ 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])) diff --git a/tests/test_project.py b/tests/test_project.py index 1e95e32f..693fec6a 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -19,7 +19,7 @@ import mesonpy -from .conftest import MESON_VERSION, PYPROJECT_METADATA_VERSION, in_git_repo_context, metadata, package_dir +from .conftest import MESON_VERSION, in_git_repo_context, metadata, package_dir def test_unsupported_python_version(package_unsupported_python_version): @@ -40,7 +40,6 @@ 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): @@ -67,7 +66,6 @@ def test_meson_build_metadata(tmp_path): ''')) -@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): @@ -96,7 +94,6 @@ def test_dynamic_license(tmp_path): ''')) -@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_list(tmp_path): diff --git a/tests/test_sdist.py b/tests/test_sdist.py index a155059f..a98b27d1 100644 --- a/tests/test_sdist.py +++ b/tests/test_sdist.py @@ -32,15 +32,9 @@ def test_pep621_metadata(sdist_full_metadata): sdist_pkg_info = sdist.extractfile('full_metadata-1.2.3/PKG-INFO').read() meta = metadata(sdist_pkg_info) - - # pyproject-metadata prior to 0.9.0 strips trailing newlines + # Including the trailing newline in the expected value is inconvenient. meta['license'] = meta['license'].rstrip() - # pyproject-metadata 0.9.0 and later does not emit Home-Page - meta.pop('home_page', None) - # nor normalizes Project-URL keys - meta['project_urls'] = {k.lower(): v for k, v in meta['project_urls'].items()} - assert meta == metadata(textwrap.dedent('''\ Metadata-Version: 2.1 Name: full-metadata diff --git a/tests/test_wheel.py b/tests/test_wheel.py index b744578c..20ae0ba7 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -16,7 +16,7 @@ import mesonpy -from .conftest import MESON_VERSION, PYPROJECT_METADATA_VERSION, adjust_packaging_platform_tag, metadata +from .conftest import MESON_VERSION, adjust_packaging_platform_tag, metadata EXT_SUFFIX = sysconfig.get_config_var('EXT_SUFFIX') @@ -136,7 +136,6 @@ def test_contents_license_file(wheel_license_file): assert artifact.read('license_file-1.0.0.dist-info/LICENSE.custom').rstrip() == b'Hello!' -@pytest.mark.xfail(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old') @pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression') def test_license_pep639(wheel_license_pep639): artifact = wheel.wheelfile.WheelFile(wheel_license_pep639) From 1e13d6c72f6cec9201e38569df90166ce71a58fd Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Tue, 19 Nov 2024 20:34:33 +0100 Subject: [PATCH 5/5] MAINT: remove unused test package --- tests/packages/missing-version/meson.build | 5 ----- tests/packages/missing-version/pyproject.toml | 10 ---------- tests/test_metadata.py | 2 +- 3 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 tests/packages/missing-version/meson.build delete mode 100644 tests/packages/missing-version/pyproject.toml diff --git a/tests/packages/missing-version/meson.build b/tests/packages/missing-version/meson.build deleted file mode 100644 index 4ccf75bc..00000000 --- a/tests/packages/missing-version/meson.build +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-FileCopyrightText: 2023 The meson-python developers -# -# SPDX-License-Identifier: MIT - -project('missing-version', version: '1.0.0') diff --git a/tests/packages/missing-version/pyproject.toml b/tests/packages/missing-version/pyproject.toml deleted file mode 100644 index 6538dd09..00000000 --- a/tests/packages/missing-version/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-FileCopyrightText: 2023 The meson-python developers -# -# SPDX-License-Identifier: MIT - -[build-system] -build-backend = 'mesonpy' -requires = ['meson-python'] - -[project] -name = 'missing-version' diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 354ae012..6609cd8f 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -51,7 +51,7 @@ def test_unsupported_dynamic(): Metadata.from_pyproject(pyproject, pathlib.Path()) -def test_missing_version(package_missing_version): +def test_missing_version(): pyproject = {'project': { 'name': 'missing-version', }}