From 882f3adf5f14832103879ced38674c7cc0b0cd3f Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Fri, 24 Feb 2023 21:18:05 +0100 Subject: [PATCH 1/6] MAINT: streamline project metadata handling some more To support "dependencies" as a dynamic filed in pyproject.toml and implement build time dependencies pins we need to rewrite this part of the metadata in the wheel builder. Move the RFC 822 serialization of the metadata closer to where it is written to the wheel archive. --- mesonpy/__init__.py | 26 ++++++++++++-------------- tests/test_tags.py | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 4b41afc59..e64e8e41b 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -219,13 +219,11 @@ class _WheelBuilder(): def __init__( self, project: Project, - metadata: Optional[pyproject_metadata.StandardMetadata], source_dir: pathlib.Path, build_dir: pathlib.Path, sources: Dict[str, Dict[str, Any]], ) -> None: self._project = project - self._metadata = metadata self._source_dir = source_dir self._build_dir = build_dir self._sources = sources @@ -316,13 +314,13 @@ def wheel(self) -> bytes: @property def entrypoints_txt(self) -> bytes: """dist-info entry_points.txt.""" - if not self._metadata: + if not self._project.metadata: return b'' - data = self._metadata.entrypoints.copy() + data = self._project.metadata.entrypoints.copy() data.update({ - 'console_scripts': self._metadata.scripts, - 'gui_scripts': self._metadata.gui_scripts, + 'console_scripts': self._project.metadata.scripts, + 'gui_scripts': self._project.metadata.gui_scripts, }) text = '' @@ -475,7 +473,7 @@ def _install_path( def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None: # add metadata - whl.writestr(f'{self.distinfo_dir}/METADATA', self._project.metadata) + whl.writestr(f'{self.distinfo_dir}/METADATA', bytes(self._project.metadata.as_rfc822())) whl.writestr(f'{self.distinfo_dir}/WHEEL', self.wheel) if self.entrypoints_txt: whl.writestr(f'{self.distinfo_dir}/entry_points.txt', self.entrypoints_txt) @@ -792,7 +790,6 @@ def _validate_metadata(self) -> None: def _wheel_builder(self) -> _WheelBuilder: return _WheelBuilder( self, - self._metadata, self._source_dir, self._build_dir, self._install_plan, @@ -887,10 +884,10 @@ def version(self) -> str: """Project version.""" return str(self._metadata.version) - @cached_property - def metadata(self) -> bytes: - """Project metadata as an RFC822 message.""" - return bytes(self._metadata.as_rfc822()) + @property + def metadata(self) -> pyproject_metadata.StandardMetadata: + """Project metadata.""" + return self._metadata @property def license_file(self) -> Optional[pathlib.Path]: @@ -960,8 +957,9 @@ def sdist(self, directory: Path) -> pathlib.Path: pkginfo_info = tarfile.TarInfo(f'{dist_name}/PKG-INFO') if mtime: pkginfo_info.mtime = mtime - pkginfo_info.size = len(self.metadata) - tar.addfile(pkginfo_info, fileobj=io.BytesIO(self.metadata)) + metadata = bytes(self._metadata.as_rfc822()) + pkginfo_info.size = len(metadata) + tar.addfile(pkginfo_info, fileobj=io.BytesIO(metadata)) return sdist diff --git a/tests/test_tags.py b/tests/test_tags.py index 5779c098b..8f66d6117 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -56,7 +56,7 @@ def wheel_builder_test_factory(monkeypatch, content): files = defaultdict(list) files.update({key: [(pathlib.Path(x), os.path.join('build', x)) for x in value] for key, value in content.items()}) monkeypatch.setattr(mesonpy._WheelBuilder, '_wheel_files', files) - return mesonpy._WheelBuilder(None, None, pathlib.Path(), pathlib.Path(), pathlib.Path()) + return mesonpy._WheelBuilder(None, pathlib.Path(), pathlib.Path(), pathlib.Path(), {}) def test_tag_empty_wheel(monkeypatch): From 8559f51328beabd55b4bbcce24373dd838a8a06d Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 1 Mar 2023 22:47:42 +0100 Subject: [PATCH 2/6] MAINT: remove unnecessary type hint --- mesonpy/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index e64e8e41b..fc2621059 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -624,7 +624,6 @@ class Project(): _ALLOWED_DYNAMIC_FIELDS: ClassVar[List[str]] = [ 'version', ] - _metadata: pyproject_metadata.StandardMetadata def __init__( self, From b21b02cce0ce0b44ce413b6fbdcc9446e7440103 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Fri, 3 Mar 2023 23:31:07 +0100 Subject: [PATCH 3/6] MAINT: refactor metadata validation Move to stand-alone function for clarity and to enable unit testing. --- mesonpy/__init__.py | 48 ++++++++++++++++++------------------------- tests/test_project.py | 6 +++--- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index fc2621059..3c9d4683b 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -57,9 +57,7 @@ if typing.TYPE_CHECKING: # pragma: no cover - from typing import ( - Any, Callable, ClassVar, DefaultDict, List, Literal, Optional, Sequence, TextIO, Tuple, Type, TypeVar, Union - ) + from typing import Any, Callable, DefaultDict, List, Literal, Optional, Sequence, TextIO, Tuple, Type, TypeVar, Union from mesonpy._compat import Iterator, ParamSpec, Path @@ -618,13 +616,28 @@ def _string_or_strings(value: Any, name: str) -> List[str]: return config -class Project(): - """Meson project wrapper to generate Python artifacts.""" +def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None: + """Validate package metadata.""" - _ALLOWED_DYNAMIC_FIELDS: ClassVar[List[str]] = [ + allowed_dynamic_fields = [ 'version', ] + # check for unsupported dynamic fields + unsupported_dynamic = {key for key in metadata.dynamic if key not in allowed_dynamic_fields} + if unsupported_dynamic: + s = ', '.join(f'"{x}"' for x in unsupported_dynamic) + raise ConfigError(f'unsupported dynamic metadata fields: {s}') + + # check if we are running on an unsupported interpreter + if metadata.requires_python: + metadata.requires_python.prereleases = True + if platform.python_version().rstrip('+') not in metadata.requires_python: + raise ConfigError(f'building with Python {platform.python_version()}, version {metadata.requires_python} required') + + +class Project(): + """Meson project wrapper to generate Python artifacts.""" def __init__( self, source_dir: Path, @@ -722,7 +735,7 @@ def __init__( '{yellow}{bold}! Using Meson to generate the project metadata ' '(no `project` section in pyproject.toml){reset}'.format(**_STYLES) ) - self._validate_metadata() + _validate_metadata(self._metadata) # set version from meson.build if dynamic if 'version' in self._metadata.dynamic: @@ -764,27 +777,6 @@ def _configure(self, reconfigure: bool = False) -> None: self._run(['meson', 'setup', *setup_args]) - def _validate_metadata(self) -> None: - """Check the pyproject.toml metadata and see if there are any issues.""" - - # check for unsupported dynamic fields - unsupported_dynamic = { - key for key in self._metadata.dynamic - if key not in self._ALLOWED_DYNAMIC_FIELDS - } - if unsupported_dynamic: - s = ', '.join(f'"{x}"' for x in unsupported_dynamic) - raise MesonBuilderError(f'Unsupported dynamic fields: {s}') - - # check if we are running on an unsupported interpreter - if self._metadata.requires_python: - self._metadata.requires_python.prereleases = True - if platform.python_version().rstrip('+') not in self._metadata.requires_python: - raise MesonBuilderError( - f'Unsupported Python version {platform.python_version()}, ' - f'expected {self._metadata.requires_python}' - ) - @cached_property def _wheel_builder(self) -> _WheelBuilder: return _WheelBuilder( diff --git a/tests/test_project.py b/tests/test_project.py index 4c68a50c0..52f5b9819 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -45,14 +45,14 @@ def test_version(package): def test_unsupported_dynamic(package_unsupported_dynamic): - with pytest.raises(mesonpy.MesonBuilderError, match='Unsupported dynamic fields: "dependencies"'): + with pytest.raises(mesonpy.ConfigError, match='unsupported dynamic metadata fields: "dependencies"'): with mesonpy.Project.with_temp_working_dir(): pass def test_unsupported_python_version(package_unsupported_python_version): - with pytest.raises(mesonpy.MesonBuilderError, match=( - f'Unsupported Python version {platform.python_version()}, expected ==1.0.0' + with pytest.raises(mesonpy.ConfigError, match=( + f'building with Python {platform.python_version()}, version ==1.0.0 required' )): with mesonpy.Project.with_temp_working_dir(): pass From 5cd2f1a62642f87812b35087fa2fca81f9d290ed Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Fri, 3 Mar 2023 23:52:37 +0100 Subject: [PATCH 4/6] TST: adapt to "dependencies" possibly being a dynamic field --- tests/packages/unsupported-dynamic/pyproject.toml | 2 +- tests/test_project.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/packages/unsupported-dynamic/pyproject.toml b/tests/packages/unsupported-dynamic/pyproject.toml index ee63c29b9..2886363e7 100644 --- a/tests/packages/unsupported-dynamic/pyproject.toml +++ b/tests/packages/unsupported-dynamic/pyproject.toml @@ -10,5 +10,5 @@ requires = ['meson-python'] name = 'unsupported-dynamic' version = '1.0.0' dynamic = [ - 'dependencies', + 'requires-python', ] diff --git a/tests/test_project.py b/tests/test_project.py index 52f5b9819..b826e4f72 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -45,7 +45,7 @@ def test_version(package): def test_unsupported_dynamic(package_unsupported_dynamic): - with pytest.raises(mesonpy.ConfigError, match='unsupported dynamic metadata fields: "dependencies"'): + with pytest.raises(mesonpy.ConfigError, match='unsupported dynamic metadata fields: "requires-python"'): with mesonpy.Project.with_temp_working_dir(): pass From e69f391d748d9aeecc6b0b0fd5ba640237feacaf Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Thu, 23 Feb 2023 22:42:10 +0100 Subject: [PATCH 5/6] 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. --- mesonpy/__init__.py | 61 +++++++++++++++++-- pyproject.toml | 4 ++ .../packages/dynamic-dependencies/meson.build | 5 ++ .../dynamic-dependencies/pyproject.toml | 27 ++++++++ tests/test_metadata.py | 13 ++++ tests/test_tags.py | 2 +- tests/test_wheel.py | 24 ++++++++ 7 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 tests/packages/dynamic-dependencies/meson.build create mode 100644 tests/packages/dynamic-dependencies/pyproject.toml diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 3c9d4683b..e583e2fee 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -14,6 +14,7 @@ import argparse import collections import contextlib +import copy import difflib import functools import importlib.machinery @@ -42,6 +43,12 @@ else: import tomllib +if sys.version_info < (3, 8): + import importlib_metadata +else: + import importlib.metadata as importlib_metadata + +import packaging.requirements import packaging.version import pyproject_metadata @@ -130,6 +137,8 @@ def _init_colors() -> Dict[str, str]: _EXTENSION_SUFFIX_REGEX = re.compile(r'^\.(?:(?P[^.]+)\.)?(?:so|pyd|dll)$') assert all(re.match(_EXTENSION_SUFFIX_REGEX, x) for x in _EXTENSION_SUFFIXES) +_REQUIREMENT_NAME_REGEX = re.compile(r'^(?P[A-Za-z0-9][A-Za-z0-9-_.]+)') + # Maps wheel installation paths to Meson installation path placeholders. # See https://docs.python.org/3/library/sysconfig.html#installation-paths @@ -220,12 +229,13 @@ def __init__( source_dir: pathlib.Path, build_dir: pathlib.Path, sources: Dict[str, Dict[str, Any]], + build_time_pins_templates: List[str], ) -> None: self._project = project self._source_dir = source_dir self._build_dir = build_dir self._sources = sources - + self._build_time_pins = build_time_pins_templates self._libs_build_dir = self._build_dir / 'mesonpy-wheel-libs' @cached_property @@ -470,8 +480,12 @@ def _install_path( wheel_file.write(origin, location) def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None: + # copute dynamic dependencies + metadata = copy.copy(self._project.metadata) + metadata.dependencies = _compute_build_time_dependencies(metadata.dependencies, self._build_time_pins) + # add metadata - whl.writestr(f'{self.distinfo_dir}/METADATA', bytes(self._project.metadata.as_rfc822())) + whl.writestr(f'{self.distinfo_dir}/METADATA', bytes(metadata.as_rfc822())) whl.writestr(f'{self.distinfo_dir}/WHEEL', self.wheel) if self.entrypoints_txt: whl.writestr(f'{self.distinfo_dir}/entry_points.txt', self.entrypoints_txt) @@ -571,7 +585,9 @@ def _strings(value: Any, name: str) -> List[str]: scheme = _table({ 'args': _table({ name: _strings for name in _MESON_ARGS_KEYS - }) + }), + 'dependencies': _strings, + 'build-time-pins': _strings, }) table = pyproject.get('tool', {}).get('meson-python', {}) @@ -620,6 +636,7 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None: """Validate package metadata.""" allowed_dynamic_fields = [ + 'dependencies', 'version', ] @@ -636,9 +653,36 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None: raise ConfigError(f'building with Python {platform.python_version()}, version {metadata.requires_python} required') +def _compute_build_time_dependencies( + dependencies: List[packaging.requirements.Requirement], + pins: List[str]) -> List[packaging.requirements.Requirement]: + for template in pins: + match = _REQUIREMENT_NAME_REGEX.match(template) + if not match: + raise ConfigError(f'invalid requirement format in "build-time-pins": {template!r}') + name = match.group(1) + try: + version = packaging.version.parse(importlib_metadata.version(name)) + except importlib_metadata.PackageNotFoundError as exc: + raise ConfigError(f'package "{name}" specified in "build-time-pins" not found: {template!r}') from exc + pin = packaging.requirements.Requirement(template.format(v=version)) + if pin.marker: + raise ConfigError(f'requirements in "build-time-pins" cannot contain markers: {template!r}') + if pin.extras: + raise ConfigError(f'requirements in "build-time-pins" cannot contain extras: {template!r}') + added = False + for d in dependencies: + if d.name == name: + d.specifier = d.specifier & pin.specifier + added = True + if not added: + dependencies.append(pin) + return dependencies + + class Project(): """Meson project wrapper to generate Python artifacts.""" - def __init__( + def __init__( # noqa: C901 self, source_dir: Path, working_dir: Path, @@ -655,6 +699,7 @@ def __init__( self._meson_cross_file = self._build_dir / 'meson-python-cross-file.ini' self._meson_args: MesonArgs = collections.defaultdict(list) self._env = os.environ.copy() + self._build_time_pins = [] _check_meson_version() @@ -741,6 +786,13 @@ def __init__( if 'version' in self._metadata.dynamic: self._metadata.version = packaging.version.Version(self._meson_version) + # set base dependencie if dynamic + if 'dependencies' in self._metadata.dynamic: + dependencies = [packaging.requirements.Requirement(d) for d in pyproject_config.get('dependencies', [])] + self._metadata.dependencies = dependencies + self._metadata.dynamic.remove('dependencies') + self._build_time_pins = pyproject_config.get('build-time-pins', []) + def _run(self, cmd: Sequence[str]) -> None: """Invoke a subprocess.""" print('{cyan}{bold}+ {}{reset}'.format(' '.join(cmd), **_STYLES)) @@ -784,6 +836,7 @@ def _wheel_builder(self) -> _WheelBuilder: self._source_dir, self._build_dir, self._install_plan, + self._build_time_pins, ) def build_commands(self, install_dir: Optional[pathlib.Path] = None) -> Sequence[Sequence[str]]: diff --git a/pyproject.toml b/pyproject.toml index 2486504d2..e0148bfff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,9 @@ build-backend = 'mesonpy' backend-path = ['.'] requires = [ + 'importlib_metadata; python_version < "3.8"', 'meson >= 0.63.3', + 'packaging', 'pyproject-metadata >= 0.7.1', 'tomli >= 1.0.0; python_version < "3.11"', 'setuptools >= 60.0; python_version >= "3.12"', @@ -29,7 +31,9 @@ classifiers = [ dependencies = [ 'colorama; os_name == "nt"', + 'importlib_metadata; python_version < "3.8"', 'meson >= 0.63.3', + 'packaging', 'pyproject-metadata >= 0.7.1', 'tomli >= 1.0.0; python_version < "3.11"', 'setuptools >= 60.0; python_version >= "3.12"', diff --git a/tests/packages/dynamic-dependencies/meson.build b/tests/packages/dynamic-dependencies/meson.build new file mode 100644 index 000000000..9f136bb9e --- /dev/null +++ b/tests/packages/dynamic-dependencies/meson.build @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2023 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('dynamic-dependencies', version: '1.0.0') diff --git a/tests/packages/dynamic-dependencies/pyproject.toml b/tests/packages/dynamic-dependencies/pyproject.toml new file mode 100644 index 000000000..4c6c5f4ae --- /dev/null +++ b/tests/packages/dynamic-dependencies/pyproject.toml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2023 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] + +[project] +name = 'dynamic-dependencies' +version = '1.0.0' +dynamic = [ + 'dependencies', +] + +[tool.meson-python] +# base dependencies, used for the sdist +dependencies = [ + 'meson >= 0.63.0', + 'meson-python >= 0.13.0', +] +# additional requirements based on the versions of the dependencies +# used during the build of the wheels, used for the wheels +build-time-pins = [ + 'meson >= {v}', + 'packaging ~= {v.major}.{v.minor}', +] diff --git a/tests/test_metadata.py b/tests/test_metadata.py index cdcbdfa27..59778253b 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -68,3 +68,16 @@ def test_dynamic_version(sdist_dynamic_version): Name: dynamic-version Version: 1.0.0 ''') + + +def test_dynamic_dependencies(sdist_dynamic_dependencies): + with tarfile.open(sdist_dynamic_dependencies, 'r:gz') as sdist: + sdist_pkg_info = sdist.extractfile('dynamic_dependencies-1.0.0/PKG-INFO').read().decode() + + assert sdist_pkg_info == textwrap.dedent('''\ + Metadata-Version: 2.1 + Name: dynamic-dependencies + Version: 1.0.0 + Requires-Dist: meson>=0.63.0 + Requires-Dist: meson-python>=0.13.0 + ''') diff --git a/tests/test_tags.py b/tests/test_tags.py index 8f66d6117..78f77d38c 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -56,7 +56,7 @@ def wheel_builder_test_factory(monkeypatch, content): files = defaultdict(list) files.update({key: [(pathlib.Path(x), os.path.join('build', x)) for x in value] for key, value in content.items()}) monkeypatch.setattr(mesonpy._WheelBuilder, '_wheel_files', files) - return mesonpy._WheelBuilder(None, pathlib.Path(), pathlib.Path(), pathlib.Path(), {}) + return mesonpy._WheelBuilder(None, pathlib.Path(), pathlib.Path(), {}, []) def test_tag_empty_wheel(monkeypatch): diff --git a/tests/test_wheel.py b/tests/test_wheel.py index da10d760f..84844eaa8 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -11,7 +11,14 @@ import sysconfig import textwrap + +if sys.version_info < (3, 8): + import importlib_metadata +else: + import importlib.metadata as importlib_metadata + import packaging.tags +import packaging.version import pytest import wheel.wheelfile @@ -240,3 +247,20 @@ def test_top_level_modules(package_module_types): 'namespace', 'native', } + + +def test_build_time_pins(wheel_dynamic_dependencies): + artifact = wheel.wheelfile.WheelFile(wheel_dynamic_dependencies) + + meson_version = packaging.version.parse(importlib_metadata.version('meson')) + packaging_version = packaging.version.parse(importlib_metadata.version('packaging')) + + with artifact.open('dynamic_dependencies-1.0.0.dist-info/METADATA') as f: + assert f.read().decode() == textwrap.dedent(f'''\ + Metadata-Version: 2.1 + Name: dynamic-dependencies + Version: 1.0.0 + Requires-Dist: meson>=0.63.0,>={meson_version} + Requires-Dist: meson-python>=0.13.0 + Requires-Dist: packaging~={packaging_version.major}.{packaging_version.minor} + ''') From 58606b943d0fbb670193cd1a42f555798e3f1772 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Mon, 6 Mar 2023 21:31:03 +0100 Subject: [PATCH 6/6] ENH: do not generate build-time pins for pre-releases It is not possible to define a universally useful semantic for build-time pins involving pre-releases. Do not generate them. --- mesonpy/__init__.py | 3 +++ tests/test_wheel.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index e583e2fee..adcc04577 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -665,6 +665,9 @@ def _compute_build_time_dependencies( version = packaging.version.parse(importlib_metadata.version(name)) except importlib_metadata.PackageNotFoundError as exc: raise ConfigError(f'package "{name}" specified in "build-time-pins" not found: {template!r}') from exc + if version.is_devrelease or version.is_prerelease: + print('meson-python: build-time pin for pre-release version "{version}" of "{name}" not generared: {template!r}') + continue pin = packaging.requirements.Requirement(template.format(v=version)) if pin.marker: raise ConfigError(f'requirements in "build-time-pins" cannot contain markers: {template!r}') diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 84844eaa8..b7b0e62c5 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -264,3 +264,29 @@ def test_build_time_pins(wheel_dynamic_dependencies): Requires-Dist: meson-python>=0.13.0 Requires-Dist: packaging~={packaging_version.major}.{packaging_version.minor} ''') + + +def test_compute_build_time_dependencies(monkeypatch): + versions = { + 'aaa': '1.2.3', + 'bbb': '4.5.6', + 'ddd': '1.0.0rc1', # pre-release will not be added to build-time dependencies + } + monkeypatch.setattr(importlib_metadata, 'version', lambda package: versions.get(package)) + deps = [ + 'bbb>=0.1', + 'ccc>=0.2', + 'ddd>=0.3', + ] + pins = [ + 'aaa>={v}', + 'bbb~={v.major}.{v.minor}', + 'ddd=={v}', + ] + r = mesonpy._compute_build_time_dependencies([packaging.requirements.Requirement(x) for x in deps], pins) + assert sorted(str(x) for x in r) == [ + 'aaa>=1.2.3', + 'bbb>=0.1,~=4.5', + 'ccc>=0.2', + 'ddd>=0.3', + ]