Skip to content

Commit

Permalink
build: validate build-system table schema
Browse files Browse the repository at this point in the history
Closes #364.
  • Loading branch information
layday committed Sep 29, 2021
1 parent cccaf93 commit c6c66f2
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 32 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -3,6 +3,16 @@ Changelog
+++++++++


Unreleased
==========

- Add schema validation for ``build-system`` table to check conformity
with PEP 517 and PEP 518 (`PR #365`_, Fixes `#364`_)

.. _PR #365: https://github.com/pypa/build/pull/365
.. _#364: https://github.com/pypa/build/issues/364


0.7.0 (16-09-2021)
==================

Expand Down
93 changes: 61 additions & 32 deletions src/build/__init__.py
Expand Up @@ -94,12 +94,30 @@ def __str__(self) -> str:
return f'Backend operation failed: {self.exception!r}'


class BuildSystemTableValidationError(BuildException):
"""
Exception raised when the ``[build-system]`` table in pyproject.toml is invalid.
"""


class TypoWarning(Warning):
"""
Warning raised when a potential typo is found
"""


@contextlib.contextmanager
def _working_directory(path: str) -> Iterator[None]:
current = os.getcwd()

os.chdir(path)

try:
yield
finally:
os.chdir(current)


def _validate_source_directory(srcdir: str) -> None:
if not os.path.isdir(srcdir):
raise BuildException(f'Source {srcdir} is not a directory')
Expand Down Expand Up @@ -153,25 +171,52 @@ def check_dependency(


def _find_typo(dictionary: Mapping[str, str], expected: str) -> None:
if expected not in dictionary:
for obj in dictionary:
if difflib.SequenceMatcher(None, expected, obj).ratio() >= 0.8:
warnings.warn(
f"Found '{obj}' in pyproject.toml, did you mean '{expected}'?",
TypoWarning,
)
for obj in dictionary:
if difflib.SequenceMatcher(None, expected, obj).ratio() >= 0.8:
warnings.warn(
f"Found '{obj}' in pyproject.toml, did you mean '{expected}'?",
TypoWarning,
)


@contextlib.contextmanager
def _working_directory(path: str) -> Iterator[None]:
current = os.getcwd()
def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Dict[str, Any]:
build_system_table: Optional[Dict[str, Any]] = pyproject_toml.get('build-system')

os.chdir(path)
# If pyproject.toml is missing (per PEP 517) or [build-system] is missing
# (per PEP 518), use default values
if build_system_table is None:
_find_typo(pyproject_toml, 'build-system')
return _DEFAULT_BACKEND

try:
yield
finally:
os.chdir(current)
# If [build-system] is present, it must have a ``requires`` field (per PEP 518)
if 'requires' not in build_system_table:
_find_typo(build_system_table, 'requires')
raise BuildSystemTableValidationError('`requires` is a required property')

if not isinstance(build_system_table['requires'], list) or not all(
isinstance(i, str) for i in build_system_table['requires']
):
raise BuildSystemTableValidationError('`requires` must be an array of strings')

if 'build-backend' not in build_system_table:
_find_typo(build_system_table, 'build-backend')
# If ``build-backend`` is missing, inject the legacy setuptools backend
# but leave ``requires`` intact to emulate pip
build_system_table['build-backend'] = _DEFAULT_BACKEND['build-backend']
elif not isinstance(build_system_table['build-backend'], str):
raise BuildSystemTableValidationError('`build-backend` must be a string')

if 'backend-path' in build_system_table and (
not isinstance(build_system_table['backend-path'], list)
or not all(isinstance(i, str) for i in build_system_table['backend-path'])
):
raise BuildSystemTableValidationError('`backend-path` must be an array of strings')

unknown_props = build_system_table.keys() - {'requires', 'build-backend', 'backend-path'}
if unknown_props:
raise BuildSystemTableValidationError('Unknown properties', unknown_props)

return build_system_table


class ProjectBuilder:
Expand Down Expand Up @@ -219,23 +264,7 @@ def __init__(
except TOMLDecodeError as e:
raise BuildException(f'Failed to parse {spec_file}: {e} ')

build_system = spec.get('build-system')
# if pyproject.toml is missing (per PEP 517) or [build-system] is missing (per PEP 518),
# use default values.
if build_system is None:
_find_typo(spec, 'build-system')
build_system = _DEFAULT_BACKEND
# if [build-system] is present, it must have a ``requires`` field (per PEP 518).
elif 'requires' not in build_system:
_find_typo(build_system, 'requires')
raise BuildException(f"Missing 'build-system.requires' in {spec_file}")
# if ``build-backend`` is missing, inject the legacy setuptools backend
# but leave ``requires`` alone to emulate pip.
elif 'build-backend' not in build_system:
_find_typo(build_system, 'build-backend')
build_system['build-backend'] = _DEFAULT_BACKEND['build-backend']

self._build_system = build_system
self._build_system = _parse_build_system_table(spec)
self._backend = self._build_system['build-backend']
self._scripts_dir = scripts_dir
self._hook_runner = runner
Expand Down
70 changes: 70 additions & 0 deletions tests/test_projectbuilder.py
Expand Up @@ -571,3 +571,73 @@ def test_log(mocker, caplog, test_flit_path):
]
if sys.version_info >= (3, 8): # stacklevel
assert [(record.lineno) for record in caplog.records] == [305, 305, 338, 368, 368, 562]


def test_parse_build_system_table_type_validation():
with pytest.raises(
build.BuildSystemTableValidationError,
match='`requires` is a required property',
):
build._parse_build_system_table(
{'build-system': {}},
)

with pytest.raises(
build.BuildSystemTableValidationError,
match='`requires` must be an array of strings',
):
build._parse_build_system_table(
{'build-system': {'requires': 'not an array'}},
)

with pytest.raises(
build.BuildSystemTableValidationError,
match='`requires` must be an array of strings',
):
build._parse_build_system_table(
{'build-system': {'requires': [1]}},
)

build._parse_build_system_table(
{'build-system': {'requires': ['foo']}},
)

with pytest.raises(
build.BuildSystemTableValidationError,
match='`build-backend` must be a string',
):
build._parse_build_system_table(
{'build-system': {'requires': ['foo'], 'build-backend': ['not a string']}},
)

build._parse_build_system_table(
{'build-system': {'requires': ['foo'], 'build-backend': 'bar'}},
)

with pytest.raises(
build.BuildSystemTableValidationError,
match='`backend-path` must be an array of strings',
):
build._parse_build_system_table(
{'build-system': {'requires': ['foo'], 'backend-path': 'not an array'}},
)

with pytest.raises(
build.BuildSystemTableValidationError,
match='`backend-path` must be an array of strings',
):
build._parse_build_system_table(
{'build-system': {'requires': ['foo'], 'backend-path': [1]}},
)

build._parse_build_system_table(
{'build-system': {'requires': ['foo'], 'build-backend': 'bar', 'backend-path': ['baz']}},
)

with pytest.raises(
build.BuildSystemTableValidationError,
match='Unknown properties',
):
build._parse_build_system_table(
{'build-system': {'requires': ['foo'], 'unknown-prop': False}},
)

0 comments on commit c6c66f2

Please sign in to comment.