Skip to content

Commit

Permalink
ENH: add support for licence and license-files dynamic fields
Browse files Browse the repository at this point in the history
Fixes #270.
  • Loading branch information
dnicolodi committed Oct 13, 2024
1 parent 010b92c commit d8b5f29
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 13 deletions.
8 changes: 8 additions & 0 deletions docs/reference/meson-compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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
Expand Down
48 changes: 46 additions & 2 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def canonicalize_license_expression(s: str) -> str:

__version__ = '0.17.0.dev0'

_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
Expand Down Expand Up @@ -272,7 +274,7 @@ def from_pyproject(
'Required "project.version" field is missing and not declared as dynamic')

# Check for unsupported dynamic fields.
unsupported_dynamic = set(metadata.dynamic) - {'version', }
unsupported_dynamic = set(metadata.dynamic) - _SUPPORTED_DYNAMIC_FIELDS
if unsupported_dynamic:
fields = ', '.join(f'"{x}"' for x in unsupported_dynamic)
raise pyproject_metadata.ConfigurationError(f'Unsupported dynamic fields: {fields}')
Expand Down Expand Up @@ -754,13 +756,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]
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 version is None:
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:
Expand Down Expand Up @@ -885,6 +904,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.
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@

import packaging.metadata
import packaging.version
import pyproject_metadata
import pytest

import mesonpy

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
Expand Down
90 changes: 89 additions & 1 deletion tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -40,6 +40,94 @@ 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_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 = []
Expand Down
4 changes: 2 additions & 2 deletions tests/test_sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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()

Expand All @@ -30,7 +30,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()

Expand Down
9 changes: 1 addition & 8 deletions tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit d8b5f29

Please sign in to comment.