summaryrefslogtreecommitdiffstats
path: root/tests/test_projectbuilder.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_projectbuilder.py')
-rw-r--r--tests/test_projectbuilder.py672
1 files changed, 672 insertions, 0 deletions
diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py
new file mode 100644
index 0000000..57ff9f9
--- /dev/null
+++ b/tests/test_projectbuilder.py
@@ -0,0 +1,672 @@
+# SPDX-License-Identifier: MIT
+
+
+import copy
+import importlib
+import logging
+import os
+import sys
+import textwrap
+
+import pep517.wrappers
+import pytest
+
+import build
+
+
+if sys.version_info >= (3, 8): # pragma: no cover
+ from importlib import metadata as importlib_metadata
+else: # pragma: no cover
+ import importlib_metadata
+
+import pathlib
+
+
+build_open_owner = 'builtins'
+
+
+DEFAULT_BACKEND = {
+ 'build-backend': 'setuptools.build_meta:__legacy__',
+ 'requires': ['setuptools >= 40.8.0', 'wheel'],
+}
+
+
+class MockDistribution(importlib_metadata.Distribution):
+ def locate_file(self, path): # pragma: no cover
+ return ''
+
+ @classmethod
+ def from_name(cls, name):
+ if name == 'extras_dep':
+ return ExtraMockDistribution()
+ elif name == 'requireless_dep':
+ return RequirelessMockDistribution()
+ elif name == 'recursive_dep':
+ return RecursiveMockDistribution()
+ elif name == 'prerelease_dep':
+ return PrereleaseMockDistribution()
+ elif name == 'circular_dep':
+ return CircularMockDistribution()
+ elif name == 'nested_circular_dep':
+ return NestedCircularMockDistribution()
+ raise importlib_metadata.PackageNotFoundError
+
+
+class ExtraMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return 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'
+ """
+ ).strip()
+
+
+class RequirelessMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: requireless_dep
+ Version: 1.0.0
+ """
+ ).strip()
+
+
+class RecursiveMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: recursive_dep
+ Version: 1.0.0
+ Requires-Dist: recursive_unmet_dep
+ """
+ ).strip()
+
+
+class PrereleaseMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: prerelease_dep
+ Version: 1.0.1a0
+ """
+ ).strip()
+
+
+class CircularMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: circular_dep
+ Version: 1.0.0
+ Requires-Dist: nested_circular_dep
+ """
+ ).strip()
+
+
+class NestedCircularMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: nested_circular_dep
+ Version: 1.0.0
+ Requires-Dist: circular_dep
+ """
+ ).strip()
+
+
+@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, requirement_string, expected):
+ monkeypatch.setattr(importlib_metadata, 'Distribution', MockDistribution)
+ assert next(build.check_dependency(requirement_string), None) == expected
+
+
+def test_bad_project(package_test_no_project):
+ # 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, package_test_flit, package_legacy, test_no_permission, package_test_bad_syntax):
+ mocker.patch('pep517.wrappers.Pep517HookCaller')
+
+ # correct flit pyproject.toml
+ builder = build.ProjectBuilder(package_test_flit)
+ pep517.wrappers.Pep517HookCaller.assert_called_with(
+ package_test_flit, 'flit_core.buildapi', backend_path=None, python_executable=sys.executable, runner=builder._runner
+ )
+ pep517.wrappers.Pep517HookCaller.reset_mock()
+
+ # custom python
+ builder = build.ProjectBuilder(package_test_flit, python_executable='some-python')
+ pep517.wrappers.Pep517HookCaller.assert_called_with(
+ package_test_flit, 'flit_core.buildapi', backend_path=None, python_executable='some-python', runner=builder._runner
+ )
+ pep517.wrappers.Pep517HookCaller.reset_mock()
+
+ # FileNotFoundError
+ builder = build.ProjectBuilder(package_legacy)
+ pep517.wrappers.Pep517HookCaller.assert_called_with(
+ package_legacy,
+ 'setuptools.build_meta:__legacy__',
+ backend_path=None,
+ python_executable=sys.executable,
+ runner=builder._runner,
+ )
+
+ # PermissionError
+ if not sys.platform.startswith('win'): # can't correctly set the permissions required for this
+ with pytest.raises(build.BuildException):
+ build.ProjectBuilder(test_no_permission)
+
+ # TomlDecodeError
+ with pytest.raises(build.BuildException):
+ build.ProjectBuilder(package_test_bad_syntax)
+
+
+@pytest.mark.parametrize('value', [b'something', 'something_else'])
+def test_python_executable(package_test_flit, value):
+ builder = build.ProjectBuilder(package_test_flit)
+
+ builder.python_executable = value
+ assert builder.python_executable == value
+ assert builder._hook.python_executable == value
+
+
+@pytest.mark.parametrize('distribution', ['wheel', 'sdist'])
+def test_get_requires_for_build_missing_backend(packages_path, distribution):
+ 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, distribution):
+ 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, distribution, tmpdir):
+ 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 test_check_dependencies(mocker, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller.get_requires_for_build_sdist')
+ mocker.patch('pep517.wrappers.Pep517HookCaller.get_requires_for_build_wheel')
+
+ builder = build.ProjectBuilder(package_test_flit)
+
+ side_effects = [
+ [],
+ ['something'],
+ pep517.wrappers.BackendUnavailable,
+ ]
+
+ builder._hook.get_requires_for_build_sdist.side_effect = copy.copy(side_effects)
+ builder._hook.get_requires_for_build_wheel.side_effect = copy.copy(side_effects)
+
+ # requires = []
+ assert builder.check_dependencies('sdist') == {('flit_core<3,>=2',)}
+ assert builder.check_dependencies('wheel') == {('flit_core<3,>=2',)}
+
+ # requires = ['something']
+ assert builder.check_dependencies('sdist') == {('flit_core<3,>=2',), ('something',)}
+ assert builder.check_dependencies('wheel') == {('flit_core<3,>=2',), ('something',)}
+
+ # BackendUnavailable
+ with pytest.raises(build.BuildBackendException):
+ builder.check_dependencies('sdist')
+ with pytest.raises(build.BuildBackendException):
+ not builder.check_dependencies('wheel')
+
+
+def test_working_directory(tmp_dir):
+ assert os.path.realpath(os.curdir) != os.path.realpath(tmp_dir)
+ with build._working_directory(tmp_dir):
+ assert os.path.realpath(os.curdir) == os.path.realpath(tmp_dir)
+
+
+def test_working_directory_exc_is_not_transformed(mocker, package_test_flit, tmp_dir):
+ mocker.patch('build._working_directory', side_effect=OSError)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ with pytest.raises(OSError):
+ builder._call_backend('build_sdist', tmp_dir)
+
+
+def test_build(mocker, package_test_flit, tmp_dir):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+ mocker.patch('build._working_directory', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+
+ builder._hook.build_sdist.side_effect = ['dist.tar.gz', Exception]
+ builder._hook.build_wheel.side_effect = ['dist.whl', Exception]
+
+ assert builder.build('sdist', tmp_dir) == os.path.join(tmp_dir, 'dist.tar.gz')
+ builder._hook.build_sdist.assert_called_with(tmp_dir, None)
+ build._working_directory.assert_called_with(package_test_flit)
+
+ assert builder.build('wheel', tmp_dir) == os.path.join(tmp_dir, 'dist.whl')
+ builder._hook.build_wheel.assert_called_with(tmp_dir, None)
+ build._working_directory.assert_called_with(package_test_flit)
+
+ with pytest.raises(build.BuildBackendException):
+ build._working_directory.assert_called_with(package_test_flit)
+ builder.build('sdist', tmp_dir)
+
+ with pytest.raises(build.BuildBackendException):
+ build._working_directory.assert_called_with(package_test_flit)
+ builder.build('wheel', tmp_dir)
+
+
+def test_default_backend(mocker, package_legacy):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_legacy)
+
+ assert builder._build_system == DEFAULT_BACKEND
+
+
+def test_missing_backend(mocker, package_test_no_backend):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', 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, package_test_no_requires):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ with pytest.raises(build.BuildException):
+ build.ProjectBuilder(package_test_no_requires)
+
+
+def test_build_system_typo(mocker, package_test_typo):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ with pytest.warns(build.TypoWarning):
+ build.ProjectBuilder(package_test_typo)
+
+
+def test_missing_outdir(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._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, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._hook.build_sdist.return_value = 'dist.tar.gz'
+
+ builder.build('sdist', '.')
+
+ builder._hook.build_sdist.assert_called_with(os.path.abspath('.'), None)
+
+
+def test_build_not_dir_outdir(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._hook.build_sdist.return_value = 'dist.tar.gz'
+ out = os.path.join(tmp_dir, 'out')
+
+ open(out, 'a').close() # create empty file
+
+ with pytest.raises(build.BuildException):
+ builder.build('sdist', out)
+
+
+@pytest.fixture(scope='session')
+def demo_pkg_inline(tmp_path_factory):
+ # 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(srcdir=os.path.join(os.path.dirname(__file__), 'packages', 'inline'))
+ out = tmp_path / 'dist'
+ builder.build('wheel', str(out))
+ return next(out.iterdir())
+
+
+@pytest.mark.isolated
+def test_build_with_dep_on_console_script(tmp_path, demo_pkg_inline, capfd, mocker):
+ """
+ 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)
+
+ 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, err = 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, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+ mocker.patch('build._working_directory', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._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')
+ builder._hook.prepare_metadata_for_build_wheel.assert_called_with(tmp_dir, None, _allow_fallback=False)
+ build._working_directory.assert_called_with(package_test_flit)
+
+
+def test_prepare_no_hook(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ failure = pep517.wrappers.HookMissing('prepare_metadata_for_build_wheel')
+ builder._hook.prepare_metadata_for_build_wheel.side_effect = failure
+
+ assert builder.prepare('wheel', tmp_dir) is None
+
+
+def test_prepare_error(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._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, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+
+ out = os.path.join(tmp_dir, 'out')
+ with open(out, 'w') as f:
+ f.write('Not a directory')
+ with pytest.raises(build.BuildException, match='Build path .* exists and is not a directory'):
+ builder.prepare('wheel', out)
+
+
+def test_no_outdir_single(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller.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, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller.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, package_test_flit):
+ def dummy_runner(cmd, cwd=None, env=None):
+ raise RuntimeError('Runner was called')
+
+ 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, package_test_no_prepare):
+ builder = build.ProjectBuilder(package_test_no_prepare)
+
+ metadata = importlib_metadata.PathDistribution(
+ pathlib.Path(builder.metadata_path(tmp_dir)),
+ ).metadata
+
+ assert metadata['name'] == 'test-no-prepare'
+ assert metadata['Version'] == '1.0.0'
+
+
+def test_metadata_path_with_prepare(tmp_dir, package_test_setuptools):
+ builder = build.ProjectBuilder(package_test_setuptools)
+
+ metadata = importlib_metadata.PathDistribution(
+ pathlib.Path(builder.metadata_path(tmp_dir)),
+ ).metadata
+
+ assert metadata['name'] == 'test-setuptools'
+ assert metadata['Version'] == '1.0.0'
+
+
+def test_metadata_path_legacy(tmp_dir, package_legacy):
+ builder = build.ProjectBuilder(package_legacy)
+
+ metadata = importlib_metadata.PathDistribution(
+ pathlib.Path(builder.metadata_path(tmp_dir)),
+ ).metadata
+
+ assert metadata['name'] == 'legacy'
+ assert metadata['Version'] == '1.0.0'
+
+
+def test_metadata_invalid_wheel(tmp_dir, package_test_bad_wheel):
+ builder = build.ProjectBuilder(package_test_bad_wheel)
+
+ with pytest.raises(ValueError, match='Invalid wheel'):
+ builder.metadata_path(tmp_dir)
+
+
+@pytest.fixture
+def mock_tomli_not_available(mocker):
+ loads = mocker.patch('tomli.loads')
+ mocker.patch.dict(sys.modules, {'tomli': None})
+ importlib.reload(build)
+ try:
+ yield
+ finally:
+ loads.assert_not_called()
+ mocker.stopall()
+ importlib.reload(build)
+
+
+@pytest.mark.skipif(sys.version_info >= (3, 11), reason='No need to test old toml support on 3.11+')
+def test_toml_instead_of_tomli(mocker, mock_tomli_not_available, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._hook.build_sdist.return_value = 'dist.tar.gz'
+
+ builder.build('sdist', '.')
+
+ builder._hook.build_sdist.assert_called_with(os.path.abspath('.'), None)
+
+
+def test_log(mocker, caplog, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', 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', '.')
+ builder.log('something')
+
+ 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...'),
+ ('INFO', 'something'),
+ ]
+ if sys.version_info >= (3, 8): # stacklevel
+ assert caplog.records[-1].lineno == 602
+
+
+@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, parse_output):
+ assert build._parse_build_system_table(pyproject_toml) == parse_output
+
+
+@pytest.mark.parametrize(
+ ('pyproject_toml', 'error_message'),
+ [
+ (
+ {'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, error_message):
+ with pytest.raises(build.BuildSystemTableValidationError, match=error_message):
+ build._parse_build_system_table(pyproject_toml)