Skip to content

Commit e3ced25

Browse files
committed
ENH: add support for PEP 639
1 parent d6b5580 commit e3ced25

File tree

8 files changed

+116
-12
lines changed

8 files changed

+116
-12
lines changed

mesonpy/__init__.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@
5353
from mesonpy._compat import cached_property, read_binary
5454

5555

56+
try:
57+
from packaging.licenses import InvalidLicenseExpression, canonicalize_license_expression
58+
except ImportError:
59+
# PEP-639 support requires packaging >= 24.2.
60+
def canonicalize_license_expression(s: str) -> str: # type: ignore[misc]
61+
warnings.warn(
62+
'canonicalization and validation of license expression in "project.license" '
63+
'as defined by PEP-639 requires packaging version 24.2 or later.', stacklevel=2)
64+
return s
65+
66+
class InvalidLicenseExpression(Exception): # type: ignore[no-redef]
67+
pass
68+
69+
5670
if typing.TYPE_CHECKING: # pragma: no cover
5771
from typing import Any, Callable, DefaultDict, Dict, List, Literal, Optional, Sequence, TextIO, Tuple, Type, TypeVar, Union
5872

@@ -251,6 +265,10 @@ def from_pyproject( # type: ignore[override]
251265
fields = ', '.join(f'"{x}"' for x in unsupported_dynamic)
252266
raise pyproject_metadata.ConfigurationError(f'Unsupported dynamic fields: {fields}')
253267

268+
# Validate license field to be a valid SDPX license expression.
269+
if isinstance(metadata.license, str):
270+
metadata.license = canonicalize_license_expression(metadata.license)
271+
254272
return metadata
255273

256274
@property
@@ -339,13 +357,6 @@ def _data_dir(self) -> str:
339357
def _libs_dir(self) -> str:
340358
return f'.{self._metadata.distribution_name}.mesonpy.libs'
341359

342-
@property
343-
def _license_file(self) -> Optional[pathlib.Path]:
344-
license_ = self._metadata.license
345-
if license_ and isinstance(license_, pyproject_metadata.License):
346-
return license_.file
347-
return None
348-
349360
@property
350361
def wheel(self) -> bytes:
351362
"""Return WHEEL file for dist-info."""
@@ -428,9 +439,17 @@ def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None:
428439
if self.entrypoints_txt:
429440
whl.writestr(f'{self._distinfo_dir}/entry_points.txt', self.entrypoints_txt)
430441

431-
# add license (see https://github.com/mesonbuild/meson-python/issues/88)
432-
if self._license_file:
433-
whl.write(self._license_file, f'{self._distinfo_dir}/{os.path.basename(self._license_file)}')
442+
# Add pre-PEP-639 license files.
443+
if isinstance(self._metadata.license, pyproject_metadata.License):
444+
license_file = self._metadata.license.file
445+
if license_file:
446+
whl.write(license_file, f'{self._distinfo_dir}/{os.path.basename(license_file)}')
447+
448+
# Add PEP-639 license-files. Use ``getattr()`` for compatibility with pyproject-metadata < 0.9.0.
449+
license_files = getattr(self._metadata, 'license_files', None)
450+
if license_files:
451+
for f in license_files:
452+
whl.write(f, f'{self._distinfo_dir}/licenses/{pathlib.Path(f).as_posix()}')
434453

435454
def build(self, directory: Path) -> pathlib.Path:
436455
wheel_file = pathlib.Path(directory, f'{self.name}.whl')
@@ -1023,7 +1042,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
10231042
warnings.showwarning = _showwarning
10241043
try:
10251044
return func(*args, **kwargs)
1026-
except (Error, pyproject_metadata.ConfigurationError) as exc:
1045+
except (Error, InvalidLicenseExpression, pyproject_metadata.ConfigurationError) as exc:
10271046
prefix = f'{style.ERROR}meson-python: error:{style.RESET} '
10281047
_log('\n' + textwrap.indent(str(exc), prefix))
10291048
raise SystemExit(1) from exc

tests/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@
2828

2929
def metadata(data):
3030
meta, other = packaging.metadata.parse_email(data)
31+
# PEP-639 support requires packaging >= 24.1. Add minimal
32+
# handling of PEP-639 fields here to allow testing with older
33+
# packaging releases.
34+
value = other.pop('license-expression', None)
35+
if value is not None:
36+
# The ``License-Expression`` header should appear only once.
37+
assert len(value) == 1
38+
meta['license-expression'] = value[0]
39+
value = other.pop('license-file', None)
40+
if value is not None:
41+
meta['license-file'] = value
3142
assert not other
3243
return meta
3344

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Placeholder, just for testing.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Placeholder, just for testing.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SPDX-FileCopyrightText: 2022 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
project('license-pep639', version: '1.0.0')
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SPDX-FileCopyrightText: 2022 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
[build-system]
6+
build-backend = 'mesonpy'
7+
requires = ['meson-python']
8+
9+
[project]
10+
name = 'license-pep639'
11+
version = '1.0.0'
12+
license = 'MIT OR BSD-3-Clause'
13+
license-files = ['LICENSES/*']

tests/test_metadata.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
from mesonpy import Metadata
1313

1414

15+
try:
16+
import packaging.licenses as packaging_licenses
17+
except ImportError:
18+
packaging_licenses = None
19+
20+
1521
def test_package_name():
1622
name = 'package.Test'
1723
metadata = Metadata(name='package.Test', version=packaging.version.Version('0.0.1'))
@@ -57,3 +63,25 @@ def test_missing_version(package_missing_version):
5763
))
5864
with pytest.raises(pyproject_metadata.ConfigurationError, match=match):
5965
Metadata.from_pyproject(pyproject, pathlib.Path())
66+
67+
68+
@pytest.mark.skipif(packaging_licenses is None, reason='packaging too old')
69+
def test_normalize_license():
70+
pyproject = {'project': {
71+
'name': 'test',
72+
'version': '1.2.3',
73+
'license': 'mit or bsd-3-clause',
74+
}}
75+
metadata = Metadata.from_pyproject(pyproject, pathlib.Path())
76+
assert metadata.license == 'MIT OR BSD-3-Clause'
77+
78+
79+
@pytest.mark.skipif(packaging_licenses is None, reason='packaging too old')
80+
def test_invalid_license():
81+
pyproject = {'project': {
82+
'name': 'test',
83+
'version': '1.2.3',
84+
'license': 'Foo',
85+
}}
86+
with pytest.raises(packaging_licenses.InvalidLicenseExpression, match='Unknown license: \'foo\''):
87+
Metadata.from_pyproject(pyproject, pathlib.Path())

tests/test_wheel.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@
1212
import textwrap
1313

1414
import packaging.tags
15+
import pyproject_metadata
1516
import pytest
1617
import wheel.wheelfile
1718

1819
import mesonpy
1920

20-
from .conftest import adjust_packaging_platform_tag
21+
from .conftest import adjust_packaging_platform_tag, metadata
2122

2223

24+
PYPROJECT_METADATA_VERSION = tuple(map(int, pyproject_metadata.__version__.split('.')[:2]))
25+
2326
_meson_ver_str = subprocess.run(['meson', '--version'], check=True, stdout=subprocess.PIPE, text=True).stdout
2427
MESON_VERSION = tuple(map(int, _meson_ver_str.split('.')[:3]))
2528

@@ -140,6 +143,29 @@ def test_contents_license_file(wheel_license_file):
140143
assert artifact.read('license_file-1.0.0.dist-info/LICENSE.custom').rstrip() == b'Hello!'
141144

142145

146+
@pytest.mark.xfail(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old')
147+
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
148+
def test_license_pep639(wheel_license_pep639):
149+
artifact = wheel.wheelfile.WheelFile(wheel_license_pep639)
150+
151+
assert wheel_contents(artifact) == {
152+
'license_pep639-1.0.0.dist-info/METADATA',
153+
'license_pep639-1.0.0.dist-info/RECORD',
154+
'license_pep639-1.0.0.dist-info/WHEEL',
155+
'license_pep639-1.0.0.dist-info/licenses/LICENSES/BSD-3-Clause.txt',
156+
'license_pep639-1.0.0.dist-info/licenses/LICENSES/MIT.txt',
157+
}
158+
159+
assert metadata(artifact.read('license_pep639-1.0.0.dist-info/METADATA')) == metadata(textwrap.dedent('''\
160+
Metadata-Version: 2.4
161+
Name: license-pep639
162+
Version: 1.0.0
163+
License-Expression: MIT OR BSD-3-Clause
164+
License-File: LICENSES/BSD-3-Clause.txt
165+
License-File: LICENSES/MIT.txt
166+
'''))
167+
168+
143169
@pytest.mark.skipif(sys.platform not in {'linux', 'darwin', 'sunos5'}, reason='Not supported on this platform')
144170
def test_contents(package_library, wheel_library):
145171
artifact = wheel.wheelfile.WheelFile(wheel_library)

0 commit comments

Comments
 (0)