Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
ENH: add support for PEP 639
  • Loading branch information
dnicolodi committed Jan 16, 2025
commit 0e013f3bd31c2ae3a2ade7f0611a6e6891d7e919
41 changes: 30 additions & 11 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions tests/packages/license-pep639/LICENSES/BSD-3-Clause.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Placeholder, just for testing.
1 change: 1 addition & 0 deletions tests/packages/license-pep639/LICENSES/MIT.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Placeholder, just for testing.
5 changes: 5 additions & 0 deletions tests/packages/license-pep639/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2022 The meson-python developers
#
# SPDX-License-Identifier: MIT

project('license-pep639', version: '1.0.0')
13 changes: 13 additions & 0 deletions tests/packages/license-pep639/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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/*']
28 changes: 28 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down Expand Up @@ -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())
28 changes: 27 additions & 1 deletion tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]))

Expand Down Expand Up @@ -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)
Expand Down