Skip to content

Commit 7c7249c

Browse files
committed
ENH: add support for licence and license-files dynamic fields
Fixes mesonbuild#270.
1 parent fa4cfc5 commit 7c7249c

File tree

6 files changed

+198
-13
lines changed

6 files changed

+198
-13
lines changed

docs/reference/meson-compatibility.rst

+8
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ versions.
4545
Meson 1.3.0 or later is required for compiling extension modules
4646
targeting the Python limited API.
4747

48+
.. option:: 1.6.0
49+
50+
Meson 1.6.0 or later is required to support ``license`` and
51+
``license-files`` dynamic fields in ``pyproject.toml`` and to
52+
populate the package license and license files from the ones
53+
declared via the ``project()`` call in ``meson.build``. This also
54+
requires ``pyproject-metadata`` version 0.9.0 or later.
55+
4856
Build front-ends by default build packages in an isolated Python
4957
environment where build dependencies are installed. Most often, unless
5058
a package or its build dependencies declare explicitly a version

mesonpy/__init__.py

+47-2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef]
7979
MesonArgs = Mapping[MesonArgsKeys, List[str]]
8080

8181

82+
_PYPROJECT_METADATA_VERSION = tuple(map(int, pyproject_metadata.__version__.split('.')[:2]))
83+
_SUPPORTED_DYNAMIC_FIELDS = {'version', } if _PYPROJECT_METADATA_VERSION < (0, 9) else {'version', 'license', 'license-files'}
84+
8285
_NINJA_REQUIRED_VERSION = '1.8.2'
8386
_MESON_REQUIRED_VERSION = '0.63.3' # keep in sync with the version requirement in pyproject.toml
8487

@@ -260,7 +263,7 @@ def from_pyproject( # type: ignore[override]
260263
metadata = super().from_pyproject(data, project_dir, metadata_version)
261264

262265
# Check for unsupported dynamic fields.
263-
unsupported_dynamic = set(metadata.dynamic) - {'version', }
266+
unsupported_dynamic = set(metadata.dynamic) - _SUPPORTED_DYNAMIC_FIELDS # type: ignore[operator]
264267
if unsupported_dynamic:
265268
fields = ', '.join(f'"{x}"' for x in unsupported_dynamic)
266269
raise pyproject_metadata.ConfigurationError(f'Unsupported dynamic fields: {fields}')
@@ -731,13 +734,30 @@ def __init__(
731734
raise pyproject_metadata.ConfigurationError(
732735
'Field "version" declared as dynamic but version is not defined in meson.build')
733736
self._metadata.version = packaging.version.Version(version)
737+
if 'license' in self._metadata.dynamic:
738+
license = self._meson_license
739+
if license is None:
740+
raise pyproject_metadata.ConfigurationError(
741+
'Field "license" declared as dynamic but license is not specified in meson.build')
742+
# mypy is not happy when analyzing typing based on
743+
# pyproject-metadata < 0.9 where license needs to be of
744+
# License type. However, this code is not executed if
745+
# pyproject-metadata is older than 0.9 because then dynamic
746+
# license is not allowed.
747+
self._metadata.license = license # type: ignore[assignment, unused-ignore]
748+
if 'license-files' in self._metadata.dynamic:
749+
self._metadata.license_files = self._meson_license_files
734750
else:
735751
# if project section is missing, use minimal metdata from meson.build
736752
name, version = self._meson_name, self._meson_version
737753
if not version:
738754
raise pyproject_metadata.ConfigurationError(
739755
'Section "project" missing in pyproject.toml and version is not defined in meson.build')
740-
self._metadata = Metadata(name=name, version=packaging.version.Version(version))
756+
kwargs = {
757+
'license': self._meson_license,
758+
'license_files': self._meson_license_files
759+
} if _PYPROJECT_METADATA_VERSION >= (0, 9) else {}
760+
self._metadata = Metadata(name=name, version=packaging.version.Version(version), **kwargs)
741761

742762
# verify that we are running on a supported interpreter
743763
if self._metadata.requires_python:
@@ -862,6 +882,31 @@ def _meson_version(self) -> Optional[str]:
862882
return None
863883
return value
864884

885+
@property
886+
def _meson_license(self) -> Optional[str]:
887+
"""The license specified with the ``license`` argument to ``project()`` in meson.build."""
888+
value = self._info('intro-projectinfo').get('license', None)
889+
if value is None:
890+
return None
891+
assert isinstance(value, list)
892+
if len(value) > 1:
893+
raise pyproject_metadata.ConfigurationError(
894+
'Using a list of strings for the license declared in meson.build is ambiguous: use a SPDX license expression')
895+
value = value[0]
896+
assert isinstance(value, str)
897+
if value == 'unknown':
898+
return None
899+
return str(canonicalize_license_expression(value)) # str() is to make mypy happy
900+
901+
@property
902+
def _meson_license_files(self) -> Optional[List[pathlib.Path]]:
903+
"""The license files specified with the ``license_files`` argument to ``project()`` in meson.build."""
904+
value = self._info('intro-projectinfo').get('license_files', None)
905+
if not value:
906+
return None
907+
assert isinstance(value, list)
908+
return [pathlib.Path(x) for x in value]
909+
865910
def sdist(self, directory: Path) -> pathlib.Path:
866911
"""Generates a sdist (source distribution) in the specified directory."""
867912
# Generate meson dist file.

tests/conftest.py

+7
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,20 @@
1919

2020
import packaging.metadata
2121
import packaging.version
22+
import pyproject_metadata
2223
import pytest
2324

2425
import mesonpy
2526

2627
from mesonpy._util import chdir
2728

2829

30+
PYPROJECT_METADATA_VERSION = tuple(map(int, pyproject_metadata.__version__.split('.')[:2]))
31+
32+
_meson_ver_str = subprocess.run(['meson', '--version'], check=True, stdout=subprocess.PIPE, text=True).stdout
33+
MESON_VERSION = tuple(map(int, _meson_ver_str.split('.')[:3]))
34+
35+
2936
def metadata(data):
3037
meta, other = packaging.metadata.parse_email(data)
3138
# PEP-639 support requires packaging >= 24.1. Add minimal

tests/test_project.py

+133-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import mesonpy
2121

22-
from .conftest import in_git_repo_context, package_dir
22+
from .conftest import MESON_VERSION, PYPROJECT_METADATA_VERSION, in_git_repo_context, metadata, package_dir
2323

2424

2525
def test_unsupported_python_version(package_unsupported_python_version):
@@ -40,6 +40,138 @@ def test_missing_dynamic_version(package_missing_dynamic_version):
4040
pass
4141

4242

43+
@pytest.mark.skipif(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old')
44+
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
45+
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
46+
def test_meson_build_metadata(tmp_path):
47+
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
48+
[build-system]
49+
build-backend = 'mesonpy'
50+
requires = ['meson-python']
51+
'''), encoding='utf8')
52+
53+
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
54+
project('test', version: '1.2.3', license: 'MIT', license_files: 'LICENSE')
55+
'''), encoding='utf8')
56+
57+
tmp_path.joinpath('LICENSE').write_text('')
58+
59+
p = mesonpy.Project(tmp_path, tmp_path / 'build')
60+
61+
assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\
62+
Metadata-Version: 2.4
63+
Name: test
64+
Version: 1.2.3
65+
License-Expression: MIT
66+
License-File: LICENSE
67+
'''))
68+
69+
70+
@pytest.mark.skipif(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old')
71+
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
72+
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
73+
def test_dynamic_license(tmp_path):
74+
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
75+
[build-system]
76+
build-backend = 'mesonpy'
77+
requires = ['meson-python']
78+
79+
[project]
80+
name = 'test'
81+
version = '1.0.0'
82+
dynamic = ['license']
83+
'''), encoding='utf8')
84+
85+
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
86+
project('test', license: 'MIT')
87+
'''), encoding='utf8')
88+
89+
p = mesonpy.Project(tmp_path, tmp_path / 'build')
90+
91+
assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\
92+
Metadata-Version: 2.4
93+
Name: test
94+
Version: 1.0.0
95+
License-Expression: MIT
96+
'''))
97+
98+
99+
@pytest.mark.skipif(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old')
100+
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
101+
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
102+
def test_dynamic_license_list(tmp_path):
103+
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
104+
[build-system]
105+
build-backend = 'mesonpy'
106+
requires = ['meson-python']
107+
108+
[project]
109+
name = 'test'
110+
version = '1.0.0'
111+
dynamic = ['license']
112+
'''), encoding='utf8')
113+
114+
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
115+
project('test', license: ['MIT', 'BSD-3-Clause'])
116+
'''), encoding='utf8')
117+
118+
with pytest.raises(pyproject_metadata.ConfigurationError, match='Using a list of strings for the license'):
119+
mesonpy.Project(tmp_path, tmp_path / 'build')
120+
121+
122+
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
123+
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
124+
def test_dynamic_license_missing(tmp_path):
125+
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
126+
[build-system]
127+
build-backend = 'mesonpy'
128+
requires = ['meson-python']
129+
130+
[project]
131+
name = 'test'
132+
version = '1.0.0'
133+
dynamic = ['license']
134+
'''), encoding='utf8')
135+
136+
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
137+
project('test')
138+
'''), encoding='utf8')
139+
140+
with pytest.raises(pyproject_metadata.ConfigurationError, match='Field "license" declared as dynamic but'):
141+
mesonpy.Project(tmp_path, tmp_path / 'build')
142+
143+
144+
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
145+
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
146+
def test_dynamic_license_files(tmp_path):
147+
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
148+
[build-system]
149+
build-backend = 'mesonpy'
150+
requires = ['meson-python']
151+
152+
[project]
153+
name = 'test'
154+
version = '1.0.0'
155+
dynamic = ['license', 'license-files']
156+
'''), encoding='utf8')
157+
158+
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
159+
project('test', license: 'MIT', license_files: ['LICENSE'])
160+
'''), encoding='utf8')
161+
162+
tmp_path.joinpath('LICENSE').write_text('')
163+
164+
p = mesonpy.Project(tmp_path, tmp_path / 'build')
165+
166+
assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\
167+
Metadata-Version: 2.4
168+
Name: test
169+
Version: 1.0.0
170+
License-Expression: MIT
171+
License-File: LICENSE
172+
'''))
173+
174+
43175
def test_user_args(package_user_args, tmp_path, monkeypatch):
44176
project_run = mesonpy.Project._run
45177
cmds = []

tests/test_sdist.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .conftest import in_git_repo_context, metadata
1717

1818

19-
def test_no_pep621(sdist_library):
19+
def test_meson_build_metadata(sdist_library):
2020
with tarfile.open(sdist_library, 'r:gz') as sdist:
2121
sdist_pkg_info = sdist.extractfile('library-1.0.0/PKG-INFO').read()
2222

@@ -27,7 +27,7 @@ def test_no_pep621(sdist_library):
2727
'''))
2828

2929

30-
def test_pep621(sdist_full_metadata):
30+
def test_pep621_metadata(sdist_full_metadata):
3131
with tarfile.open(sdist_full_metadata, 'r:gz') as sdist:
3232
sdist_pkg_info = sdist.extractfile('full_metadata-1.2.3/PKG-INFO').read()
3333

tests/test_wheel.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,19 @@
66
import re
77
import shutil
88
import stat
9-
import subprocess
109
import sys
1110
import sysconfig
1211
import textwrap
1312

1413
import packaging.tags
15-
import pyproject_metadata
1614
import pytest
1715
import wheel.wheelfile
1816

1917
import mesonpy
2018

21-
from .conftest import adjust_packaging_platform_tag, metadata
19+
from .conftest import MESON_VERSION, PYPROJECT_METADATA_VERSION, adjust_packaging_platform_tag, metadata
2220

2321

24-
PYPROJECT_METADATA_VERSION = tuple(map(int, pyproject_metadata.__version__.split('.')[:2]))
25-
26-
_meson_ver_str = subprocess.run(['meson', '--version'], check=True, stdout=subprocess.PIPE, text=True).stdout
27-
MESON_VERSION = tuple(map(int, _meson_ver_str.split('.')[:3]))
28-
2922
EXT_SUFFIX = sysconfig.get_config_var('EXT_SUFFIX')
3023
if sys.version_info <= (3, 8, 7):
3124
if MESON_VERSION >= (0, 99):

0 commit comments

Comments
 (0)