Skip to content

Commit 547b563

Browse files
committed
BUG: improve validation of pyproject.toml meson-python configuration
Switch from an incomplete (an bugged) ad hoc validation to a scheme based validation strategy. The scheme is defined in the function _validate_pyproject_config() as nested dictionaries where the keys are configuration field names and valued are validation functions. Unknown fields result in an error. Fixes #293.
1 parent 31bfd31 commit 547b563

File tree

2 files changed

+89
-32
lines changed

2 files changed

+89
-32
lines changed

mesonpy/__init__.py

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,36 @@ def build_editable(self, directory: Path, verbose: bool = False) -> pathlib.Path
654654
return wheel_file
655655

656656

657+
def _validate_pyproject_config(pyproject: Dict[str, Any]) -> Dict[str, Any]:
658+
659+
def _table(scheme: Dict[str, Callable[[Any, str], Any]]) -> Callable[[Any, str], Dict[str, Any]]:
660+
def func(value: Any, name: str) -> Dict[str, Any]:
661+
if not isinstance(value, dict):
662+
raise ConfigError(f'Configuration entry "{name}" must be a table')
663+
table = {}
664+
for key, val in value.items():
665+
check = scheme.get(key)
666+
if check is None:
667+
raise ConfigError(f'Unknown configuration entry "{name}.{key}"')
668+
table[key] = check(val, f'{name}.{key}')
669+
return table
670+
return func
671+
672+
def _strings(value: Any, name: str) -> List[str]:
673+
if not isinstance(value, list) or not all(isinstance(x, str) for x in value):
674+
raise ConfigError(f'Configuration entry "{name}" must be a list of strings')
675+
return value
676+
677+
scheme = _table({
678+
'args': _table({
679+
name: _strings for name in _MESON_ARGS_KEYS
680+
})
681+
})
682+
683+
table = pyproject.get('tool', {}).get('meson-python', {})
684+
return scheme(table, 'tool.meson-python')
685+
686+
657687
class Project():
658688
"""Meson project wrapper to generate Python artifacts."""
659689

@@ -662,7 +692,7 @@ class Project():
662692
]
663693
_metadata: Optional[pyproject_metadata.StandardMetadata]
664694

665-
def __init__( # noqa: C901
695+
def __init__(
666696
self,
667697
source_dir: Path,
668698
working_dir: Path,
@@ -712,11 +742,13 @@ def __init__( # noqa: C901
712742
self._meson_cross_file.write_text(cross_file_data)
713743
self._meson_args['setup'].extend(('--cross-file', os.fspath(self._meson_cross_file)))
714744

715-
# load config -- PEP 621 support is optional
716-
self._config = tomllib.loads(self._source_dir.joinpath('pyproject.toml').read_text())
717-
self._pep621 = 'project' in self._config
745+
# load pyproject.toml
746+
pyproject = tomllib.loads(self._source_dir.joinpath('pyproject.toml').read_text())
747+
748+
# package metadata
749+
self._pep621 = 'project' in pyproject:
718750
if self.pep621:
719-
self._metadata = pyproject_metadata.StandardMetadata.from_pyproject(self._config, self._source_dir)
751+
self._metadata = pyproject_metadata.StandardMetadata.from_pyproject(pyproject, self._source_dir)
720752
else:
721753
print(
722754
'{yellow}{bold}! Using Meson to generate the project metadata '
@@ -727,14 +759,10 @@ def __init__( # noqa: C901
727759
if self._metadata:
728760
self._validate_metadata()
729761

730-
# load meson args
731-
for key in self._get_config_key('args'):
732-
self._meson_args[key].extend(self._get_config_key(f'args.{key}'))
733-
# XXX: We should validate the user args to make sure they don't conflict with ours.
734-
735-
self._check_for_unknown_config_keys({
736-
'args': _MESON_ARGS_KEYS,
737-
})
762+
# load meson args from pyproject.toml
763+
pyproject_config = _validate_pyproject_config(pyproject)
764+
for key, value in pyproject_config.get('args', {}).items():
765+
self._meson_args[key].extend(value)
738766

739767
# meson arguments from the command line take precedence over
740768
# arguments from the configuration file thus are added later
@@ -768,14 +796,6 @@ def __init__( # noqa: C901
768796
if self._metadata and 'version' in self._metadata.dynamic:
769797
self._metadata.version = self.version
770798

771-
def _get_config_key(self, key: str) -> Any:
772-
value: Any = self._config
773-
for part in f'tool.meson-python.{key}'.split('.'):
774-
if not isinstance(value, Mapping):
775-
raise ConfigError(f'Configuration entry "tool.meson-python.{key}" should be a TOML table not {type(value)}')
776-
value = value.get(part, {})
777-
return value
778-
779799
def _run(self, cmd: Sequence[str]) -> None:
780800
"""Invoke a subprocess."""
781801
print('{cyan}{bold}+ {}{reset}'.format(' '.join(cmd), **_STYLES))
@@ -834,17 +854,6 @@ def _validate_metadata(self) -> None:
834854
f'expected {self._metadata.requires_python}'
835855
)
836856

837-
def _check_for_unknown_config_keys(self, valid_args: Mapping[str, Collection[str]]) -> None:
838-
config = self._config.get('tool', {}).get('meson-python', {})
839-
840-
for key, valid_subkeys in config.items():
841-
if key not in valid_args:
842-
raise ConfigError(f'Unknown configuration key "tool.meson-python.{key}"')
843-
844-
for subkey in valid_args[key]:
845-
if subkey not in valid_subkeys:
846-
raise ConfigError(f'Unknown configuration key "tool.meson-python.{key}.{subkey}"')
847-
848857
@cached_property
849858
def _wheel_builder(self) -> _WheelBuilder:
850859
return _WheelBuilder(

tests/test_project.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
# SPDX-License-Identifier: MIT
44

55
import platform
6+
import sys
7+
import textwrap
8+
9+
10+
if sys.version_info < (3, 11):
11+
import tomli as tomllib
12+
else:
13+
import tomllib
614

715
import pytest
816

@@ -99,3 +107,43 @@ def test_install_tags(package_purelib_and_platlib, tmp_path_session):
99107
}
100108
)
101109
assert project.is_pure
110+
111+
112+
def test_validate_pyproject_config_one():
113+
pyproject_config = tomllib.loads(textwrap.dedent('''
114+
[tool.meson-python.args]
115+
setup = ['-Dfoo=true']
116+
'''))
117+
conf = mesonpy._validate_pyproject_config(pyproject_config)
118+
assert conf['args'] == {'setup': ['-Dfoo=true']}
119+
120+
121+
def test_validate_pyproject_config_all():
122+
pyproject_config = tomllib.loads(textwrap.dedent('''
123+
[tool.meson-python.args]
124+
setup = ['-Dfoo=true']
125+
dist = []
126+
compile = ['-j4']
127+
install = ['--tags=python']
128+
'''))
129+
conf = mesonpy._validate_pyproject_config(pyproject_config)
130+
assert conf['args'] == {
131+
'setup': ['-Dfoo=true'],
132+
'dist': [],
133+
'compile': ['-j4'],
134+
'install': ['--tags=python']}
135+
136+
137+
def test_validate_pyproject_config_unknown():
138+
pyproject_config = tomllib.loads(textwrap.dedent('''
139+
[tool.meson-python.args]
140+
invalid = true
141+
'''))
142+
with pytest.raises(mesonpy.ConfigError, match='unknown configuration entry "tool.meson-python.args.invalid"'):
143+
mesonpy._validate_pyproject_config(pyproject_config)
144+
145+
146+
def test_validate_pyproject_config_empty():
147+
pyproject_config = tomllib.loads(textwrap.dedent(''))
148+
config = mesonpy._validate_pyproject_config(pyproject_config)
149+
assert config == {}

0 commit comments

Comments
 (0)