/
niceSOFT
/
python3-build
Обзор
Документация
Войти
/
niceSOFT
/
python3-build
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/test_projectbuilder.py
781 строка
29 KB
Henry Schreiner
perf: memoize check_dependency (#1122)
07 июл 2026, 23:39
Не верифицирован
07 июл 2026, 23:39
820d4e5
Код
Авторство
О чём код?
# SPDX-License-Identifier: MIT from __future__ import annotations import copy import logging import os import pathlib import sys import textwrap import typing import zipfile from collections.abc import Callable from typing import TYPE_CHECKING, NoReturn import pyproject_hooks import pytest import pytest_mock import build import build._builder from build._compat import importlib as _importlib if TYPE_CHECKING: from collections.abc import Mapping from build._builder import BuildSystemTable, TOMLValue build_open_owner = 'builtins' DEFAULT_BACKEND = { 'build-backend': 'setuptools.build_meta:__legacy__', 'requires': ['setuptools >= 40.8.0'], } class MockDistribution(_importlib.metadata.Distribution): _metadata: str = '' def locate_file(self, path: str | os.PathLike[str]) -> _importlib.metadata.SimplePath: # pragma: no cover raise NotImplementedError def read_text(self, filename: str) -> str: if filename == 'METADATA': return self._metadata return '' @classmethod def from_name(cls, name: str) -> MockDistribution: registry: dict[str, type[MockDistribution]] = { 'extras_dep': ExtraMockDistribution, 'requireless_dep': RequirelessMockDistribution, 'recursive_dep': RecursiveMockDistribution, 'prerelease_dep': PrereleaseMockDistribution, 'circular_dep': CircularMockDistribution, 'nested_circular_dep': NestedCircularMockDistribution, } if (dist_cls := registry.get(name)) is not None: return dist_cls() raise _importlib.metadata.PackageNotFoundError class ExtraMockDistribution(MockDistribution): _metadata = textwrap.dedent("""\ Metadata-Version: 2.2 Name: extras_dep Version: 1.0.0 Provides-Extra: extra-without-associated-deps Provides-Extra: extra-with_unmet-deps Requires-Dist: unmet_dep; extra == 'extra-with-unmet-deps' Provides-Extra: extra-with-met-deps Requires-Dist: extras_dep; extra == 'extra-with-met-deps' Provides-Extra: recursive-extra-with-unmet-deps Requires-Dist: recursive_dep; extra == 'recursive-extra-with-unmet-deps'""") class RequirelessMockDistribution(MockDistribution): _metadata = textwrap.dedent("""\ Metadata-Version: 2.2 Name: requireless_dep Version: 1.0.0""") class RecursiveMockDistribution(MockDistribution): _metadata = textwrap.dedent("""\ Metadata-Version: 2.2 Name: recursive_dep Version: 1.0.0 Requires-Dist: recursive_unmet_dep""") class PrereleaseMockDistribution(MockDistribution): _metadata = textwrap.dedent("""\ Metadata-Version: 2.2 Name: prerelease_dep Version: 1.0.1a0""") class CircularMockDistribution(MockDistribution): _metadata = textwrap.dedent("""\ Metadata-Version: 2.2 Name: circular_dep Version: 1.0.0 Requires-Dist: nested_circular_dep""") class NestedCircularMockDistribution(MockDistribution): _metadata = textwrap.dedent("""\ Metadata-Version: 2.2 Name: nested_circular_dep Version: 1.0.0 Requires-Dist: circular_dep""") @pytest.mark.parametrize( ('requirement_string', 'expected'), [ ('extras_dep', None), ('missing_dep', ('missing_dep',)), ('requireless_dep', None), ('extras_dep[undefined_extra]', None), # would the wheel builder filter this out? ('extras_dep[extra-without-associated-deps]', None), ( 'extras_dep[extra-with-unmet-deps]', ('extras_dep[extra-with-unmet-deps]', 'unmet_dep; extra == "extra-with-unmet-deps"'), ), ( 'extras_dep[recursive-extra-with-unmet-deps]', ( 'extras_dep[recursive-extra-with-unmet-deps]', 'recursive_dep; extra == "recursive-extra-with-unmet-deps"', 'recursive_unmet_dep', ), ), ('extras_dep[extra-with-met-deps]', None), ('missing_dep; python_version>"10"', None), ('missing_dep; python_version<="1"', None), ('missing_dep; python_version>="1"', ('missing_dep; python_version >= "1"',)), ('extras_dep == 1.0.0', None), ('extras_dep == 2.0.0', ('extras_dep==2.0.0',)), ('extras_dep[extra-without-associated-deps] == 1.0.0', None), ('extras_dep[extra-without-associated-deps] == 2.0.0', ('extras_dep[extra-without-associated-deps]==2.0.0',)), ('prerelease_dep >= 1.0.0', None), ('circular_dep', None), ], ) def test_check_dependency(monkeypatch: pytest.MonkeyPatch, requirement_string: str, expected: tuple[str, ...] | None) -> None: monkeypatch.setattr(_importlib.metadata, 'Distribution', MockDistribution) assert next(build.check_dependency(requirement_string), None) == expected def test_check_dependency_diamond_visits_each_once(monkeypatch: pytest.MonkeyPatch) -> None: # A→B,C; B→D; C→D. The shared subtree (D) must be verified at most once, # not once per path, otherwise dense graphs blow up exponentially. graph = { 'diamond_a': 'Requires-Dist: diamond_b\nRequires-Dist: diamond_c', 'diamond_b': 'Requires-Dist: diamond_d', 'diamond_c': 'Requires-Dist: diamond_d', 'diamond_d': '', } lookups: dict[str, int] = {} class DiamondDistribution(MockDistribution): _name = '' def read_text(self, filename: str) -> str: if filename == 'METADATA': return f'Metadata-Version: 2.2\nName: {self._name}\nVersion: 1.0.0\n{graph[self._name]}' return '' @classmethod def from_name(cls, name: str) -> DiamondDistribution: # everything looked up is in the graph, so no not-found branch needed lookups[name] = lookups.get(name, 0) + 1 dist = cls() dist._name = name return dist monkeypatch.setattr(_importlib.metadata, 'Distribution', DiamondDistribution) assert list(build.check_dependency('diamond_a')) == [] assert lookups == {'diamond_a': 1, 'diamond_b': 1, 'diamond_c': 1, 'diamond_d': 1} @pytest.mark.parametrize( ('requirement_string', 'expected'), [ ('extras_dep == 2.0.0', [('extras_dep==2.0.0',)]), ('recursive_dep', [('recursive_dep', 'recursive_unmet_dep')]), ], ) def test_check_dependency_exhausted( monkeypatch: pytest.MonkeyPatch, requirement_string: str, expected: list[tuple[str, ...]] ) -> None: # Unlike ``next``-based checks, exhausting the generator runs the code past # each yield, including the unsatisfied-subtree exit. monkeypatch.setattr(_importlib.metadata, 'Distribution', MockDistribution) assert list(build.check_dependency(requirement_string)) == expected def test_bad_project(package_test_no_project: str) -> None: # Passing a nonexistent project directory with pytest.raises(build.BuildException): build.ProjectBuilder(os.path.join(package_test_no_project, 'does-not-exist')) # Passing a file as a project directory with pytest.raises(build.BuildException): build.ProjectBuilder(os.path.join(package_test_no_project, 'empty.txt')) # Passing a project directory with no pyproject.toml or setup.py with pytest.raises(build.BuildException): build.ProjectBuilder(package_test_no_project) def test_init( mocker: pytest_mock.MockerFixture, package_test_flit: str, package_legacy: str, package_test_bad_syntax: str, ) -> None: mock_buildcaller = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True) # correct flit pyproject.toml builder = build.ProjectBuilder(package_test_flit) mock_buildcaller.assert_called_with( package_test_flit, 'flit_core.buildapi', backend_path=None, python_executable=sys.executable, runner=builder._runner ) mock_buildcaller.reset_mock() # custom python builder = build.ProjectBuilder(package_test_flit, python_executable='some-python') assert builder.python_executable == 'some-python' mock_buildcaller.assert_called_with( package_test_flit, 'flit_core.buildapi', backend_path=None, python_executable='some-python', runner=builder._runner ) mock_buildcaller.reset_mock() # FileNotFoundError builder = build.ProjectBuilder(package_legacy) mock_buildcaller.assert_called_with( package_legacy, 'setuptools.build_meta:__legacy__', backend_path=None, python_executable=sys.executable, runner=builder._runner, ) # TomlDecodeError with pytest.raises(build.BuildException): build.ProjectBuilder(package_test_bad_syntax) @pytest.mark.skipif(sys.platform.startswith('win'), reason="can't correctly set the permissions required for this") def test_init_permission_error(test_no_permission: str) -> None: # pragma: win32 no cover with pytest.raises(build.BuildException): build.ProjectBuilder(test_no_permission) def test_init_makes_source_dir_absolute(package_test_flit: str) -> None: rel_dir = os.path.relpath(package_test_flit, os.getcwd()) assert not os.path.isabs(rel_dir) builder = build.ProjectBuilder(rel_dir) assert os.path.isabs(builder.source_dir) @pytest.mark.parametrize('distribution', ['wheel', 'sdist']) def test_get_requires_for_build_missing_backend( packages_path: str, distribution: typing.Literal['sdist', 'wheel', 'editable'] ) -> None: bad_backend_path = os.path.join(packages_path, 'test-bad-backend') builder = build.ProjectBuilder(bad_backend_path) with pytest.raises(build.BuildBackendException): builder.get_requires_for_build(distribution) @pytest.mark.parametrize('distribution', ['wheel', 'sdist']) def test_get_requires_for_build_missing_optional_hooks( package_test_optional_hooks: str, distribution: typing.Literal['sdist', 'wheel', 'editable'] ) -> None: builder = build.ProjectBuilder(package_test_optional_hooks) assert builder.get_requires_for_build(distribution) == set() @pytest.mark.parametrize('distribution', ['wheel', 'sdist']) def test_build_missing_backend( packages_path: str, distribution: typing.Literal['sdist', 'wheel', 'editable'], tmpdir: str ) -> None: bad_backend_path = os.path.join(packages_path, 'test-bad-backend') builder = build.ProjectBuilder(bad_backend_path) with pytest.raises(build.BuildBackendException): builder.build(distribution, str(tmpdir)) def _nothing_installed(name: str) -> NoReturn: raise _importlib.metadata.PackageNotFoundError(name) def test_check_dependencies( mocker: pytest_mock.MockerFixture, package_test_flit: str, monkeypatch: pytest.MonkeyPatch ) -> None: get_requires_sdist = mocker.patch('pyproject_hooks.BuildBackendHookCaller.get_requires_for_build_sdist') get_requires_wheel = mocker.patch('pyproject_hooks.BuildBackendHookCaller.get_requires_for_build_wheel') monkeypatch.setattr(_importlib.metadata, 'distribution', _nothing_installed) builder = build.ProjectBuilder(package_test_flit) side_effects = [ [], ['something'], pyproject_hooks.BackendUnavailable, ] get_requires_sdist.side_effect = copy.copy(side_effects) get_requires_wheel.side_effect = copy.copy(side_effects) # requires = [] assert builder.check_dependencies('sdist') == {('flit_core<4,>=2',)} assert builder.check_dependencies('wheel') == {('flit_core<4,>=2',)} # requires = ['something'] assert builder.check_dependencies('sdist') == {('flit_core<4,>=2',), ('something',)} assert builder.check_dependencies('wheel') == {('flit_core<4,>=2',), ('something',)} # BackendUnavailable with pytest.raises(build.BuildBackendException): builder.check_dependencies('sdist') with pytest.raises(build.BuildBackendException): not builder.check_dependencies('wheel') def test_build(mocker: pytest_mock.MockerFixture, package_test_flit: str, tmp_dir: str) -> None: hook = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True).return_value builder = build.ProjectBuilder(package_test_flit) hook.build_sdist.side_effect = ['dist.tar.gz', Exception] hook.build_wheel.side_effect = ['dist.whl', Exception] assert builder.build('sdist', tmp_dir) == os.path.join(tmp_dir, 'dist.tar.gz') hook.build_sdist.assert_called_with(tmp_dir, None) assert builder.build('wheel', tmp_dir) == os.path.join(tmp_dir, 'dist.whl') hook.build_wheel.assert_called_with(tmp_dir, None) with pytest.raises(build.BuildBackendException): builder.build('sdist', tmp_dir) with pytest.raises(build.BuildBackendException): builder.build('wheel', tmp_dir) def test_default_backend(mocker: pytest_mock.MockerFixture, package_legacy: str) -> None: mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True) builder = build.ProjectBuilder(package_legacy) assert builder._build_system == DEFAULT_BACKEND def test_missing_backend(mocker: pytest_mock.MockerFixture, package_test_no_backend: str) -> None: mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True) builder = build.ProjectBuilder(package_test_no_backend) assert builder._build_system == {'requires': [], 'build-backend': DEFAULT_BACKEND['build-backend']} def test_missing_requires(mocker: pytest_mock.MockerFixture, package_test_no_requires: str) -> None: mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True) with pytest.raises(build.BuildException): build.ProjectBuilder(package_test_no_requires) def test_build_system_typo(mocker: pytest_mock.MockerFixture, package_test_typo: str) -> None: mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True) with pytest.warns(build.TypoWarning): build.ProjectBuilder(package_test_typo) def test_missing_outdir(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: hook = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True).return_value builder = build.ProjectBuilder(package_test_flit) hook.build_sdist.return_value = 'dist.tar.gz' out = os.path.join(tmp_dir, 'out') builder.build('sdist', out) assert os.path.isdir(out) def test_relative_outdir(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: # noqa: ARG001 hook = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True).return_value builder = build.ProjectBuilder(package_test_flit) hook.build_sdist.return_value = 'dist.tar.gz' builder.build('sdist', '.') hook.build_sdist.assert_called_with(os.path.abspath('.'), None) def test_build_not_dir_outdir(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: hook = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True).return_value builder = build.ProjectBuilder(package_test_flit) hook.build_sdist.return_value = 'dist.tar.gz' out = os.path.join(tmp_dir, 'out') open(out, 'a', encoding='utf-8').close() # create empty file with pytest.raises(build.BuildException): builder.build('sdist', out) @pytest.fixture(scope='session') def demo_pkg_inline(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path: # builds a wheel without any dependencies and with a console script demo-pkg-inline tmp_path = tmp_path_factory.mktemp('demo-pkg-inline') builder = build.ProjectBuilder(source_dir=os.path.join(os.path.dirname(__file__), 'packages', 'inline')) out = tmp_path / 'dist' builder.build('wheel', str(out)) return next(out.iterdir()) @pytest.mark.contextvars @pytest.mark.isolated def test_build_with_dep_on_console_script( tmp_path: pathlib.Path, demo_pkg_inline: pathlib.Path, capfd: pytest.CaptureFixture[str], mocker: pytest_mock.MockerFixture ) -> None: """All command-line scripts provided by the build-required packages must be present in the build environment's PATH.""" # we first install demo pkg inline as build dependency (as this provides a console script we can check) # to validate backend invocations contain the correct path we use an inline backend that will fail, but first # provides the PATH information (and validates shutil.which is able to discover the executable - as PEP states) toml = textwrap.dedent( """ [build-system] requires = ["demo_pkg_inline"] build-backend = "build" backend-path = ["."] [project] description = "Factory ⸻ A code generator 🏭" authors = [{name = "Łukasz Langa"}] """ ) code = textwrap.dedent( """ import os import shutil import sys print("BB " + os.environ["PATH"]) exe_at = shutil.which("demo-pkg-inline") print("BB " + exe_at) """ ) (tmp_path / 'pyproject.toml').write_text(toml, encoding='UTF-8') (tmp_path / 'build.py').write_text(code, encoding='utf-8') deps = {str(demo_pkg_inline)} # we patch the requires demo_pkg_inline to refer to the wheel -> we don't need index mocker.patch('build.ProjectBuilder.build_system_requires', new_callable=mocker.PropertyMock, return_value=deps) from build.__main__ import main with pytest.raises(SystemExit): main(['--wheel', '--outdir', str(tmp_path / 'dist'), str(tmp_path)]) out, _ = capfd.readouterr() lines = [line[3:] for line in out.splitlines() if line.startswith('BB ')] # filter for our markers path_vars = lines[0].split(os.pathsep) which_detected = lines[1] assert which_detected.startswith(path_vars[0]), out def test_prepare(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: hook = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True).return_value builder = build.ProjectBuilder(package_test_flit) hook.prepare_metadata_for_build_wheel.return_value = 'dist-1.0.dist-info' assert builder.prepare('wheel', tmp_dir) == os.path.join(tmp_dir, 'dist-1.0.dist-info') hook.prepare_metadata_for_build_wheel.assert_called_with(tmp_dir, None, _allow_fallback=False) def test_prepare_no_hook(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: hook = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True).return_value builder = build.ProjectBuilder(package_test_flit) failure = pyproject_hooks.HookMissing('prepare_metadata_for_build_wheel') hook.prepare_metadata_for_build_wheel.side_effect = failure assert builder.prepare('wheel', tmp_dir) is None def test_prepare_error(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: hook = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True).return_value builder = build.ProjectBuilder(package_test_flit) hook.prepare_metadata_for_build_wheel.side_effect = Exception with pytest.raises(build.BuildBackendException, match='Backend operation failed: Exception'): builder.prepare('wheel', tmp_dir) def test_prepare_not_dir_outdir(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True) builder = build.ProjectBuilder(package_test_flit) out = os.path.join(tmp_dir, 'out') with open(out, 'w', encoding='utf-8') as f: f.write('Not a directory') with pytest.raises(build.BuildException, match=r'Build path .* exists and is not a directory'): builder.prepare('wheel', out) def test_prepare_not_dir_parent_outdir(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True) builder = build.ProjectBuilder(package_test_flit) parent = os.path.join(tmp_dir, 'parent') with open(parent, 'w', encoding='utf-8') as f: f.write('Not a directory') with pytest.raises(build.BuildException, match=r'Build path .* does not exist and cannot be a directory'): builder.prepare('wheel', os.path.join(parent, 'out')) def test_no_outdir_single(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: mocker.patch('pyproject_hooks.BuildBackendHookCaller.prepare_metadata_for_build_wheel', return_value='') builder = build.ProjectBuilder(package_test_flit) out = os.path.join(tmp_dir, 'out') builder.prepare('wheel', out) assert os.path.isdir(out) def test_no_outdir_multiple(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: mocker.patch('pyproject_hooks.BuildBackendHookCaller.prepare_metadata_for_build_wheel', return_value='') builder = build.ProjectBuilder(package_test_flit) out = os.path.join(tmp_dir, 'does', 'not', 'exist') builder.prepare('wheel', out) assert os.path.isdir(out) def test_runner_user_specified(tmp_dir: str, package_test_flit: str) -> None: def dummy_runner( cmd: typing.Sequence[str], # noqa: ARG001 cwd: str | None = None, # noqa: ARG001 extra_environ: typing.Mapping[str, str] | None = None, # noqa: ARG001 ) -> None: msg = 'Runner was called' raise RuntimeError(msg) builder = build.ProjectBuilder(package_test_flit, runner=dummy_runner) with pytest.raises(build.BuildBackendException, match='Runner was called'): builder.build('wheel', tmp_dir) def test_metadata_path_no_prepare(tmp_dir: str, package_test_no_prepare: str) -> None: builder = build.ProjectBuilder(package_test_no_prepare) metadata = _importlib.metadata.PathDistribution( pathlib.Path(builder.metadata_path(tmp_dir)), ).metadata assert metadata is not None assert metadata['name'] == 'test-no-prepare' assert metadata['Version'] == '1.0.0' def test_metadata_path_with_prepare(tmp_dir: str, package_test_setuptools: str) -> None: builder = build.ProjectBuilder(package_test_setuptools) metadata = _importlib.metadata.PathDistribution( pathlib.Path(builder.metadata_path(tmp_dir)), ).metadata assert metadata is not None # Setuptools < v69.0.3 (https://github.com/pypa/setuptools/pull/4159) normalized this to dashes assert metadata['name'].replace('-', '_') == 'test_setuptools' assert metadata['Version'] == '1.0.0' def test_metadata_path_legacy(tmp_dir: str, package_legacy: str) -> None: builder = build.ProjectBuilder(package_legacy) metadata = _importlib.metadata.PathDistribution( pathlib.Path(builder.metadata_path(tmp_dir)), ).metadata assert metadata is not None assert metadata['name'] == 'legacy' assert metadata['Version'] == '1.0.0' def test_metadata_invalid_wheel(tmp_dir: str, package_test_bad_wheel: str) -> None: builder = build.ProjectBuilder(package_test_bad_wheel) with pytest.raises(build.BuildException, match='Invalid wheel'): builder.metadata_path(tmp_dir) @pytest.mark.parametrize('distinfo_dirs', [[], ['foo-1.0.dist-info', 'bar-2.0.dist-info']], ids=['zero', 'two']) def test_metadata_path_ambiguous_dist_info( mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str, distinfo_dirs: list[str] ) -> None: # A wheel must contain exactly one dist-info directory; anything else is rejected. hook = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True).return_value builder = build.ProjectBuilder(package_test_flit) hook.prepare_metadata_for_build_wheel.side_effect = pyproject_hooks.HookMissing('prepare_metadata_for_build_wheel') wheel_name = 'foo-1.0-py3-none-any.whl' def fake_build_wheel(outdir: str, config_settings: Mapping[str, str] | None = None) -> str: # noqa: ARG001 with zipfile.ZipFile(os.path.join(outdir, wheel_name), 'w') as zf: zf.writestr('foo/__init__.py', '') for distinfo_dir in distinfo_dirs: zf.writestr(f'{distinfo_dir}/METADATA', 'Metadata-Version: 2.1\n') return wheel_name hook.build_wheel.side_effect = fake_build_wheel with pytest.raises(build.BuildException, match='Invalid wheel'): builder.metadata_path(tmp_dir) def test_metadata_path_no_prepare_build_tag(mocker: pytest_mock.MockerFixture, tmp_dir: str, package_test_flit: str) -> None: # Regression test for a wheel filename with a build tag (e.g. ``foo-1.0-1-py3-none-any.whl``): # the dist-info directory name must be read from the wheel's contents, not guessed from the # (ambiguous) filename, since ``-1`` could be parsed as either the build tag or part of the version. hook = mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True).return_value builder = build.ProjectBuilder(package_test_flit) hook.prepare_metadata_for_build_wheel.side_effect = pyproject_hooks.HookMissing('prepare_metadata_for_build_wheel') wheel_name = 'foo-1.0-1-py3-none-any.whl' def fake_build_wheel(outdir: str, config_settings: Mapping[str, str] | None = None) -> str: # noqa: ARG001 with zipfile.ZipFile(os.path.join(outdir, wheel_name), 'w') as zf: zf.writestr('foo-1.0.dist-info/METADATA', 'Metadata-Version: 2.1\nName: foo\nVersion: 1.0\n') zf.writestr('foo/__init__.py', '') return wheel_name hook.build_wheel.side_effect = fake_build_wheel metadata_dir = builder.metadata_path(tmp_dir) assert os.path.basename(metadata_dir) == 'foo-1.0.dist-info' assert os.path.isdir(metadata_dir) assert os.path.isfile(os.path.join(metadata_dir, 'METADATA')) def test_log(mocker: pytest_mock.MockerFixture, caplog: pytest.LogCaptureFixture, package_test_flit: str) -> None: mocker.patch('pyproject_hooks.BuildBackendHookCaller', autospec=True) mocker.patch('build.ProjectBuilder._call_backend', return_value='some_path') caplog.set_level(logging.DEBUG) builder = build.ProjectBuilder(package_test_flit) builder.get_requires_for_build('sdist') builder.get_requires_for_build('wheel') builder.prepare('wheel', '.') builder.build('sdist', '.') builder.build('wheel', '.') assert [(record.levelname, record.message) for record in caplog.records] == [ ('INFO', 'Getting build dependencies for sdist...'), ('INFO', 'Getting build dependencies for wheel...'), ('INFO', 'Getting metadata for wheel...'), ('INFO', 'Building sdist...'), ('INFO', 'Building wheel...'), ] @pytest.mark.parametrize( ('pyproject_toml', 'parse_output'), [ ( {'build-system': {'requires': ['foo']}}, {'requires': ['foo'], 'build-backend': 'setuptools.build_meta:__legacy__'}, ), ( {'build-system': {'requires': ['foo'], 'build-backend': 'bar'}}, {'requires': ['foo'], 'build-backend': 'bar'}, ), ( {'build-system': {'requires': ['foo'], 'build-backend': 'bar', 'backend-path': ['baz']}}, {'requires': ['foo'], 'build-backend': 'bar', 'backend-path': ['baz']}, ), ], ) def test_parse_valid_build_system_table_type(pyproject_toml: Mapping[str, TOMLValue], parse_output: BuildSystemTable) -> None: assert build._builder._parse_build_system_table(pyproject_toml) == parse_output def test_parse_default_build_system_table_not_shared() -> None: # A caller mutating the returned default table must not affect later callers. first = build._builder._parse_build_system_table({}) first['requires'].append('mutated') second = build._builder._parse_build_system_table({}) assert second['requires'] == ['setuptools >= 40.8.0'] @pytest.mark.parametrize( ('pyproject_toml', 'error_message'), [ ( {'build-system': 'not a table'}, '`build-system` must be a table', ), ( {'build-system': {}}, '`requires` is a required property', ), ( {'build-system': {'requires': 'not an array'}}, '`requires` must be an array of strings', ), ( {'build-system': {'requires': [1]}}, '`requires` must be an array of strings', ), ( {'build-system': {'requires': ['foo'], 'build-backend': ['not a string']}}, '`build-backend` must be a string', ), ( {'build-system': {'requires': ['foo'], 'backend-path': 'not an array'}}, '`backend-path` must be an array of strings', ), ( {'build-system': {'requires': ['foo'], 'backend-path': [1]}}, '`backend-path` must be an array of strings', ), ( {'build-system': {'requires': ['foo'], 'unknown-prop': False}}, 'Unknown properties: unknown-prop', ), ], ) def test_parse_invalid_build_system_table_type(pyproject_toml: Mapping[str, TOMLValue], error_message: str) -> None: with pytest.raises(build.BuildSystemTableValidationError, match=error_message): build._builder._parse_build_system_table(pyproject_toml) @pytest.mark.parametrize( 'setup', [ pytest.param(lambda _tmp_path: None, id='nonexistent'), pytest.param(lambda tmp_path: (tmp_path / 'bad').write_text('', encoding='utf-8'), id='file-not-dir'), ], ) def test_backend_path_invalid_directory(tmp_path: pathlib.Path, setup: Callable[[pathlib.Path], None]) -> None: (tmp_path / 'pyproject.toml').write_text( textwrap.dedent("""\ [build-system] requires = [] build-backend = "backend" backend-path = ["bad"] """), encoding='utf-8', ) setup(tmp_path) with pytest.raises(build.BuildSystemTableValidationError, match='does not exist or is not a directory'): build.ProjectBuilder(tmp_path)