Skip to content

Commit 4fb7b60

Browse files
committed
ENH: add support for licence and license-files dynamic fields
Fixes #270.
1 parent 5077094 commit 4fb7b60

File tree

6 files changed

+139
-12
lines changed

6 files changed

+139
-12
lines changed

docs/reference/meson-compatibility.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ versions.
3939
Meson 1.3.0 or later is required for compiling extension modules
4040
targeting the Python limited API.
4141

42+
.. option:: 1.6.0
43+
44+
Meson 1.6.0 or later is required to support ``license`` and
45+
``license-files`` dynamic fields in ``pyproject.toml`` and to
46+
populate the package license and license files from the ones
47+
declared via the ``project()`` call in ``meson.build``.
48+
4249
Build front-ends by default build packages in an isolated Python
4350
environment where build dependencies are installed. Most often, unless
4451
a package or its build dependencies declare explicitly a version

mesonpy/__init__.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def from_pyproject(
272272
'Required "project.version" field is missing and not declared as dynamic')
273273

274274
# Check for unsupported dynamic fields.
275-
unsupported_dynamic = set(metadata.dynamic) - {'version', }
275+
unsupported_dynamic = set(metadata.dynamic) - {'version', 'license', 'license-files'}
276276
if unsupported_dynamic:
277277
fields = ', '.join(f'"{x}"' for x in unsupported_dynamic)
278278
raise pyproject_metadata.ConfigurationError(f'Unsupported dynamic fields: {fields}')
@@ -753,13 +753,23 @@ def __init__(
753753
raise pyproject_metadata.ConfigurationError(
754754
'Field "version" declared as dynamic but version is not defined in meson.build')
755755
self._metadata.version = packaging.version.Version(version)
756+
if 'license' in self._metadata.dynamic:
757+
license = self._meson_license
758+
if license is None:
759+
raise pyproject_metadata.ConfigurationError(
760+
'Field "license" declared as dynamic but license is not specified in meson.build')
761+
self._metadata.license = license
762+
if 'license-files' in self._metadata.dynamic:
763+
self._metadata.license_files = self._meson_license_files
756764
else:
757765
# if project section is missing, use minimal metdata from meson.build
758766
name, version = self._meson_name, self._meson_version
759767
if version is None:
760768
raise pyproject_metadata.ConfigurationError(
761769
'Section "project" missing in pyproject.toml and version is not defined in meson.build')
762-
self._metadata = Metadata(name=name, version=packaging.version.Version(version))
770+
self._metadata = Metadata(
771+
name=name, version=packaging.version.Version(version),
772+
license=self._meson_license, license_files=self._meson_license_files)
763773

764774
# verify that we are running on a supported interpreter
765775
if self._metadata.requires_python:
@@ -884,6 +894,28 @@ def _meson_version(self) -> Optional[str]:
884894
return None
885895
return value
886896

897+
@property
898+
def _meson_license(self) -> Optional[str]:
899+
"""The license specified with the ``license`` argument to ``project()`` in meson.build."""
900+
value = self._info('intro-projectinfo').get('license', None)
901+
if value is None:
902+
return None
903+
assert isinstance(value, list)
904+
if len(value) > 1:
905+
raise ConfigurationError('using a list of strings for the license declared in meson.build is ambiguous: use a SPDX license expression')
906+
value = value[0]
907+
assert isinstance(value, str)
908+
if value == 'unknown':
909+
return None
910+
return canonicalize_license_expression(value)
911+
912+
@property
913+
def _meson_license_files(self) -> List[pathlib.Path]:
914+
"""The license files specified with the ``license_files`` argument to ``project()`` in meson.build."""
915+
value = self._info('intro-projectinfo').get('license_files', [])
916+
assert isinstance(value, list)
917+
return [pathlib.Path(x) for x in value]
918+
887919
def sdist(self, directory: Path) -> pathlib.Path:
888920
"""Generates a sdist (source distribution) in the specified directory."""
889921
# Generate meson dist file.

tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
from mesonpy._util import chdir
2626

2727

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

tests/test_project.py

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@
1414
else:
1515
import tomllib
1616

17-
import pyproject_metadata
1817
import pytest
1918

2019
import mesonpy
2120

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

2423

2524
def test_unsupported_python_version(package_unsupported_python_version):
@@ -40,6 +39,94 @@ def test_missing_dynamic_version(package_missing_dynamic_version):
4039
pass
4140

4241

42+
@pytest.mark.skipif(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old')
43+
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
44+
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
45+
def test_meson_build_metadata(tmp_path):
46+
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
47+
[build-system]
48+
build-backend = 'mesonpy'
49+
requires = ['meson-python']
50+
'''), encoding='utf8')
51+
52+
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
53+
project('test', version: '1.2.3', license: 'MIT', license_files: 'LICENSE')
54+
'''), encoding='utf8')
55+
56+
tmp_path.joinpath('LICENSE').write_text('')
57+
58+
p = mesonpy.Project(tmp_path, tmp_path / 'build')
59+
60+
assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\
61+
Metadata-Version: 2.4
62+
Name: test
63+
Version: 1.2.3
64+
License-Expression: MIT
65+
License-File: LICENSE
66+
'''))
67+
68+
69+
@pytest.mark.skipif(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old')
70+
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
71+
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
72+
def test_dynamic_license(tmp_path):
73+
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
74+
[build-system]
75+
build-backend = 'mesonpy'
76+
requires = ['meson-python']
77+
78+
[project]
79+
name = 'test'
80+
version = '1.0.0'
81+
dynamic = ['license']
82+
'''), encoding='utf8')
83+
84+
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
85+
project('test', license: 'MIT')
86+
'''), encoding='utf8')
87+
88+
p = mesonpy.Project(tmp_path, tmp_path / 'build')
89+
90+
assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\
91+
Metadata-Version: 2.4
92+
Name: test
93+
Version: 1.0.0
94+
License-Expression: MIT
95+
'''))
96+
97+
98+
@pytest.mark.skipif(PYPROJECT_METADATA_VERSION < (0, 9), reason='pyproject-metadata too old')
99+
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
100+
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
101+
def test_dynamic_license_files(tmp_path):
102+
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
103+
[build-system]
104+
build-backend = 'mesonpy'
105+
requires = ['meson-python']
106+
107+
[project]
108+
name = 'test'
109+
version = '1.0.0'
110+
dynamic = ['license', 'license-files']
111+
'''), encoding='utf8')
112+
113+
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
114+
project('test', license: 'MIT', license_files: ['LICENSE'])
115+
'''), encoding='utf8')
116+
117+
tmp_path.joinpath('LICENSE').write_text('')
118+
119+
p = mesonpy.Project(tmp_path, tmp_path / 'build')
120+
121+
assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\
122+
Metadata-Version: 2.4
123+
Name: test
124+
Version: 1.0.0
125+
License-Expression: MIT
126+
License-File: LICENSE
127+
'''))
128+
129+
43130
def test_user_args(package_user_args, tmp_path, monkeypatch):
44131
project_run = mesonpy.Project._run
45132
cmds = []

tests/test_sdist.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .conftest import in_git_repo_context, metadata
2020

2121

22-
def test_no_pep621(sdist_library):
22+
def test_meson_build_metadata(sdist_library):
2323
with tarfile.open(sdist_library, 'r:gz') as sdist:
2424
sdist_pkg_info = sdist.extractfile('library-1.0.0/PKG-INFO').read()
2525

@@ -30,7 +30,7 @@ def test_no_pep621(sdist_library):
3030
'''))
3131

3232

33-
def test_pep621(sdist_full_metadata):
33+
def test_pep621_metadata(sdist_full_metadata):
3434
with tarfile.open(sdist_full_metadata, 'r:gz') as sdist:
3535
sdist_pkg_info = sdist.extractfile('full_metadata-1.2.3/PKG-INFO').read()
3636

tests/test_wheel.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,9 @@
1818

1919
import mesonpy
2020

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

2323

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-
2924
EXT_SUFFIX = sysconfig.get_config_var('EXT_SUFFIX')
3025
if sys.version_info <= (3, 8, 7):
3126
if MESON_VERSION >= (0, 99):

0 commit comments

Comments
 (0)