Skip to content

Commit bd9bfd0

Browse files
committed
ENH: add support for wheel build-time dependencies version pins
When "dependencies" is specified as a dynamic field in the "[project]" section in pyproject.toml, the dependencies reported for the sdist are copied from the "dependencies" field in the "[tool.meson-python]" section. More importantly, the dependencies reported for the wheels are computed combining this field and the "build-time-pins" field in the same section completed with the build time version information. The "dependencies" and "build-time-pins" fields in the "[tool.meson-python]" section accept the standard metadata dependencies syntax as specified in PEP 440. The "build-time-pins" field cannot contain markers or extras but it is expanded as a format string where the 'v' variable is bound to the version of the package to which the dependency requirements applies present at the time of the build parsed as a packaging.version.Version object.
1 parent 8dde579 commit bd9bfd0

File tree

7 files changed

+131
-5
lines changed

7 files changed

+131
-5
lines changed

mesonpy/__init__.py

+57-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import argparse
1515
import collections
1616
import contextlib
17+
import copy
1718
import difflib
1819
import functools
1920
import importlib.machinery
@@ -42,6 +43,12 @@
4243
else:
4344
import tomllib
4445

46+
if sys.version_info < (3, 8):
47+
import importlib_metadata
48+
else:
49+
import importlib.metadata as importlib_metadata
50+
51+
import packaging.requirements
4552
import packaging.version
4653
import pyproject_metadata
4754

@@ -129,6 +136,8 @@ def _init_colors() -> Dict[str, str]:
129136
_EXTENSION_SUFFIX_REGEX = re.compile(r'^\.(?:(?P<abi>[^.]+)\.)?(?:so|pyd|dll)$')
130137
assert all(re.match(_EXTENSION_SUFFIX_REGEX, x) for x in _EXTENSION_SUFFIXES)
131138

139+
_REQUIREMENT_NAME_REGEX = re.compile(r'^(?P<name>[A-Za-z0-9][A-Za-z0-9-_.]+)')
140+
132141

133142
# Maps wheel installation paths to Meson installation path placeholders.
134143
# See https://docs.python.org/3/library/sysconfig.html#installation-paths
@@ -218,12 +227,13 @@ def __init__(
218227
source_dir: pathlib.Path,
219228
build_dir: pathlib.Path,
220229
sources: Dict[str, Dict[str, Any]],
230+
build_time_pins_templates: List[str],
221231
) -> None:
222232
self._project = project
223233
self._source_dir = source_dir
224234
self._build_dir = build_dir
225235
self._sources = sources
226-
236+
self._build_time_pins = build_time_pins_templates
227237
self._libs_build_dir = self._build_dir / 'mesonpy-wheel-libs'
228238

229239
@cached_property
@@ -469,8 +479,12 @@ def _install_path(
469479
wheel_file.write(origin, location)
470480

471481
def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None:
482+
# copute dynamic dependencies
483+
metadata = copy.copy(self._project.metadata)
484+
metadata.dependencies = _compute_build_time_dependencies(metadata.dependencies, self._build_time_pins)
485+
472486
# add metadata
473-
whl.writestr(f'{self.distinfo_dir}/METADATA', bytes(self._project.metadata.as_rfc822()))
487+
whl.writestr(f'{self.distinfo_dir}/METADATA', bytes(metadata.as_rfc822()))
474488
whl.writestr(f'{self.distinfo_dir}/WHEEL', self.wheel)
475489
if self.entrypoints_txt:
476490
whl.writestr(f'{self.distinfo_dir}/entry_points.txt', self.entrypoints_txt)
@@ -570,7 +584,9 @@ def _strings(value: Any, name: str) -> List[str]:
570584
scheme = _table({
571585
'args': _table({
572586
name: _strings for name in _MESON_ARGS_KEYS
573-
})
587+
}),
588+
'dependencies': _strings,
589+
'build-time-pins': _strings,
574590
})
575591

576592
table = pyproject.get('tool', {}).get('meson-python', {})
@@ -619,6 +635,7 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
619635
"""Validate package metadata."""
620636

621637
allowed_dynamic_fields = [
638+
'dependencies',
622639
'version',
623640
]
624641

@@ -635,9 +652,36 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
635652
raise ConfigError(f'building with Python {platform.python_version()}, version {metadata.requires_python} required')
636653

637654

655+
def _compute_build_time_dependencies(
656+
dependencies: List[packaging.requirements.Requirement],
657+
pins: List[str]) -> List[packaging.requirements.Requirement]:
658+
for template in pins:
659+
match = _REQUIREMENT_NAME_REGEX.match(template)
660+
if not match:
661+
raise ConfigError(f'invalid requirement format in "build-time-pins": {template!r}')
662+
name = match.group(1)
663+
try:
664+
version = packaging.version.parse(importlib_metadata.version(name))
665+
except importlib_metadata.PackageNotFoundError as exc:
666+
raise ConfigError(f'package "{name}" specified in "build-time-pins" not found: {template!r}') from exc
667+
pin = packaging.requirements.Requirement(template.format(v=version))
668+
if pin.marker:
669+
raise ConfigError(f'requirements in "build-time-pins" cannot contain markers: {template!r}')
670+
if pin.extras:
671+
raise ConfigError(f'requirements in "build-time-pins" cannot contain extras: {template!r}')
672+
added = False
673+
for d in dependencies:
674+
if d.name == name:
675+
d.specifier = d.specifier & pin.specifier
676+
added = True
677+
if not added:
678+
dependencies.append(pin)
679+
return dependencies
680+
681+
638682
class Project():
639683
"""Meson project wrapper to generate Python artifacts."""
640-
def __init__(
684+
def __init__( # noqa: C901
641685
self,
642686
source_dir: Path,
643687
working_dir: Path,
@@ -654,6 +698,7 @@ def __init__(
654698
self._meson_cross_file = self._build_dir / 'meson-python-cross-file.ini'
655699
self._meson_args: MesonArgs = collections.defaultdict(list)
656700
self._env = os.environ.copy()
701+
self._build_time_pins = []
657702

658703
_check_meson_version()
659704

@@ -740,6 +785,13 @@ def __init__(
740785
if 'version' in self._metadata.dynamic:
741786
self._metadata.version = packaging.version.Version(self._meson_version)
742787

788+
# set base dependencie if dynamic
789+
if 'dependencies' in self._metadata.dynamic:
790+
dependencies = [packaging.requirements.Requirement(d) for d in pyproject_config.get('dependencies', [])]
791+
self._metadata.dependencies = dependencies
792+
self._metadata.dynamic.remove('dependencies')
793+
self._build_time_pins = pyproject_config.get('build-time-pins', [])
794+
743795
def _run(self, cmd: Sequence[str]) -> None:
744796
"""Invoke a subprocess."""
745797
print('{cyan}{bold}+ {}{reset}'.format(' '.join(cmd), **_STYLES))
@@ -783,6 +835,7 @@ def _wheel_builder(self) -> _WheelBuilder:
783835
self._source_dir,
784836
self._build_dir,
785837
self._install_plan,
838+
self._build_time_pins,
786839
)
787840

788841
def build_commands(self, install_dir: Optional[pathlib.Path] = None) -> Sequence[Sequence[str]]:

pyproject.toml

+4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
build-backend = 'mesonpy'
77
backend-path = ['.']
88
requires = [
9+
'importlib_metadata; python_version < "3.8"',
910
'meson >= 0.63.3',
11+
'packaging',
1012
'pyproject-metadata >= 0.7.1',
1113
'tomli >= 1.0.0; python_version < "3.11"',
1214
'setuptools >= 60.0; python_version >= "3.12"',
@@ -29,7 +31,9 @@ classifiers = [
2931

3032
dependencies = [
3133
'colorama; os_name == "nt"',
34+
'importlib_metadata; python_version < "3.8"',
3235
'meson >= 0.63.3',
36+
'packaging',
3337
'pyproject-metadata >= 0.7.1',
3438
'tomli >= 1.0.0; python_version < "3.11"',
3539
'setuptools >= 60.0; python_version >= "3.12"',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SPDX-FileCopyrightText: 2023 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
project('dynamic-dependencies', version: '1.0.0')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-FileCopyrightText: 2023 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 = 'dynamic-dependencies'
11+
version = '1.0.0'
12+
dynamic = [
13+
'dependencies',
14+
]
15+
16+
[tool.meson-python]
17+
# base dependencies, used for the sdist
18+
dependencies = [
19+
'meson >= 0.63.0',
20+
'meson-python >= 0.13.0',
21+
]
22+
# additional requirements based on the versions of the dependencies
23+
# used during the build of the wheels, used for the wheels
24+
build-time-pins = [
25+
'meson >= {v}',
26+
'packaging ~= {v.major}.{v.minor}',
27+
]

tests/test_metadata.py

+13
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,16 @@ def test_dynamic_version(sdist_dynamic_version):
6868
Name: dynamic-version
6969
Version: 1.0.0
7070
''')
71+
72+
73+
def test_dynamic_dependencies(sdist_dynamic_dependencies):
74+
with tarfile.open(sdist_dynamic_dependencies, 'r:gz') as sdist:
75+
sdist_pkg_info = sdist.extractfile('dynamic_dependencies-1.0.0/PKG-INFO').read().decode()
76+
77+
assert sdist_pkg_info == textwrap.dedent('''\
78+
Metadata-Version: 2.1
79+
Name: dynamic-dependencies
80+
Version: 1.0.0
81+
Requires-Dist: meson>=0.63.0
82+
Requires-Dist: meson-python>=0.13.0
83+
''')

tests/test_tags.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def wheel_builder_test_factory(monkeypatch, content):
5656
files = defaultdict(list)
5757
files.update({key: [(pathlib.Path(x), os.path.join('build', x)) for x in value] for key, value in content.items()})
5858
monkeypatch.setattr(mesonpy._WheelBuilder, '_wheel_files', files)
59-
return mesonpy._WheelBuilder(None, pathlib.Path(), pathlib.Path(), pathlib.Path(), {})
59+
return mesonpy._WheelBuilder(None, pathlib.Path(), pathlib.Path(), pathlib.Path(), {}, [])
6060

6161

6262
def test_tag_empty_wheel(monkeypatch):

tests/test_wheel.py

+24
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
import sysconfig
1212
import textwrap
1313

14+
15+
if sys.version_info < (3, 8):
16+
import importlib_metadata
17+
else:
18+
import importlib.metadata as importlib_metadata
19+
1420
import packaging.tags
21+
import packaging.version
1522
import pytest
1623
import wheel.wheelfile
1724

@@ -239,3 +246,20 @@ def test_top_level_modules(package_module_types):
239246
'namespace',
240247
'native',
241248
}
249+
250+
251+
def test_build_time_pins(wheel_dynamic_dependencies):
252+
artifact = wheel.wheelfile.WheelFile(wheel_dynamic_dependencies)
253+
254+
meson_version = packaging.version.parse(importlib_metadata.version('meson'))
255+
packaging_version = packaging.version.parse(importlib_metadata.version('packaging'))
256+
257+
with artifact.open('dynamic_dependencies-1.0.0.dist-info/METADATA') as f:
258+
assert f.read().decode() == textwrap.dedent(f'''\
259+
Metadata-Version: 2.1
260+
Name: dynamic-dependencies
261+
Version: 1.0.0
262+
Requires-Dist: meson>=0.63.0,>={meson_version}
263+
Requires-Dist: meson-python>=0.13.0
264+
Requires-Dist: packaging~={packaging_version.major}.{packaging_version.minor}
265+
''')

0 commit comments

Comments
 (0)