Skip to content

ENH: raise SystemExit when calling meson fails #231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 48 additions & 31 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
import warnings

from typing import (
Any, ClassVar, DefaultDict, Dict, List, Optional, Sequence, Set, TextIO,
Tuple, Type, Union
Any, Callable, ClassVar, DefaultDict, Dict, List, Optional, Sequence, Set,
TextIO, Tuple, Type, TypeVar, Union
)


Expand All @@ -49,7 +49,7 @@
import mesonpy._wheelfile

from mesonpy._compat import (
Collection, Iterator, Literal, Mapping, Path, typing_get_args
Collection, Iterator, Literal, Mapping, ParamSpec, Path, typing_get_args
)


Expand All @@ -67,6 +67,7 @@


_COLORS = {
'red': '\33[31m',
'cyan': '\33[36m',
'yellow': '\33[93m',
'light_blue': '\33[94m',
Expand Down Expand Up @@ -137,11 +138,16 @@ def _setup_cli() -> None:
colorama.init() # fix colors on windows


class ConfigError(Exception):
class Error(RuntimeError):
def __str__(self) -> str:
return str(self.args[0])


class ConfigError(Error):
"""Error in the backend configuration."""


class MesonBuilderError(Exception):
class MesonBuilderError(Error):
"""Error when building the Meson package."""


Expand Down Expand Up @@ -581,7 +587,7 @@ def __init__( # noqa: C901
if archflags is not None:
arch, *other = filter(None, (x.strip() for x in archflags.split('-arch')))
if other:
raise ConfigError(f'multi-architecture builds are not supported but $ARCHFLAGS={archflags!r}')
raise ConfigError(f'Multi-architecture builds are not supported but $ARCHFLAGS={archflags!r}')
macver, _, nativearch = platform.mac_ver()
if arch != nativearch:
x = self._env.setdefault('_PYTHON_HOST_PLATFORM', f'macosx-{macver}-{arch}')
Expand Down Expand Up @@ -670,17 +676,16 @@ def _get_config_key(self, key: str) -> Any:
value: Any = self._config
for part in f'tool.meson-python.{key}'.split('.'):
if not isinstance(value, Mapping):
raise ConfigError(
f'Found unexpected value in `{part}` when looking for '
f'config key `tool.meson-python.{key}` (`{value}`)'
)
raise ConfigError(f'Configuration entry "tool.meson-python.{key}" should be a TOML table not {type(value)}')
value = value.get(part, {})
return value

def _proc(self, *args: str) -> None:
"""Invoke a subprocess."""
print('{cyan}{bold}+ {}{reset}'.format(' '.join(args), **_STYLES))
subprocess.check_call(list(args), env=self._env)
r = subprocess.run(list(args), env=self._env)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend always specifying check= to subprocess.run to clarify intent. There's a check (bugbear, perhaps?) that looks for this.

if r.returncode != 0:
raise SystemExit(r.returncode)

def _meson(self, *args: str) -> None:
"""Invoke Meson."""
Expand Down Expand Up @@ -726,29 +731,28 @@ def _validate_metadata(self) -> None:
if key not in self._ALLOWED_DYNAMIC_FIELDS
}
if unsupported_dynamic:
raise MesonBuilderError('Unsupported dynamic fields: {}'.format(
', '.join(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}`'
f'Unsupported Python version {platform.python_version()}, '
f'expected {self._metadata.requires_python}'
)

def _check_for_unknown_config_keys(self, valid_args: Mapping[str, Collection[str]]) -> None:
config = self._config.get('tool', {}).get('meson-python', {})

for key, valid_subkeys in config.items():
if key not in valid_args:
raise ConfigError(f'Unknown configuration key: tool.meson-python.{key}')
raise ConfigError(f'Unknown configuration key "tool.meson-python.{key}"')

for subkey in valid_args[key]:
if subkey not in valid_subkeys:
raise ConfigError(f'Unknown configuration key: tool.meson-python.{key}.{subkey}')
raise ConfigError(f'Unknown configuration key "tool.meson-python.{key}.{subkey}"')

@cached_property
def _wheel_builder(self) -> _WheelBuilder:
Expand Down Expand Up @@ -970,10 +974,10 @@ def _project(config_settings: Optional[Dict[Any, Any]]) -> Iterator[Project]:
builddir_value = config_settings.get('builddir', {})
if len(builddir_value) > 0:
if len(builddir_value) != 1:
raise ConfigError('Specified multiple values for `builddir`, only one is allowed')
raise ConfigError('Only one value for configuration entry "builddir" can be specified')
builddir = builddir_value[0]
if not isinstance(builddir, str):
raise ConfigError(f'Config option `builddir` should be a string (found `{type(builddir)}`)')
raise ConfigError(f'Configuration entry "builddir" should be a string not {type(builddir)}')
else:
builddir = None

Expand All @@ -984,14 +988,8 @@ def _validate_string_collection(key: str) -> None:
for item in config_settings.get(key, ())
)))
if problematic_items:
raise ConfigError(
f'Config option `{key}` should only contain string items, but '
'contains the following parameters that do not meet this criteria:' +
''.join((
f'\t- {item} (type: {type(item)})'
for item in problematic_items
))
)
s = ', '.join(f'"{item}" ({type(item)})' for item in problematic_items)
raise ConfigError(f'Configuration entries for "{key}" must be strings but contain: {s}')

meson_args_keys = typing_get_args(MesonArgsKeys)
meson_args_cli_keys = tuple(f'{key}-args' for key in meson_args_keys)
Expand All @@ -1002,10 +1000,10 @@ def _validate_string_collection(key: str) -> None:
import difflib
matches = difflib.get_close_matches(key, known_keys, n=3)
if len(matches):
postfix = f'Did you mean one of: {matches}'
alternatives = ' or '.join(f'"{match}"' for match in matches)
raise ConfigError(f'Unknown configuration entry "{key}". Did you mean {alternatives}?')
else:
postfix = 'There are no close valid keys.'
raise ConfigError(f'Unknown config setting: {key!r}. {postfix}')
raise ConfigError(f'Unknown configuration entry "{key}"')

for key in meson_args_cli_keys:
_validate_string_collection(key)
Expand Down Expand Up @@ -1045,12 +1043,29 @@ def _env_ninja_command(*, version: str = _NINJA_REQUIRED_VERSION) -> Optional[pa
return None


P = ParamSpec('P')
T = TypeVar('T')


def _pyproject_hook(func: Callable[P, T]) -> Callable[P, T]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
try:
return func(*args, **kwargs)
except Error as exc:
print('{red}meson-python: error:{reset} {msg}'.format(msg=str(exc), **_STYLES))
raise SystemExit(1)
return wrapper


@_pyproject_hook
def get_requires_for_build_sdist(
config_settings: Optional[Dict[str, str]] = None,
) -> List[str]:
return [_depstr.ninja] if _env_ninja_command() is None else []


@_pyproject_hook
def build_sdist(
sdist_directory: str,
config_settings: Optional[Dict[Any, Any]] = None,
Expand All @@ -1062,6 +1077,7 @@ def build_sdist(
return project.sdist(out).name


@_pyproject_hook
def get_requires_for_build_wheel(
config_settings: Optional[Dict[str, str]] = None,
) -> List[str]:
Expand All @@ -1087,6 +1103,7 @@ def get_requires_for_build_wheel(
return dependencies


@_pyproject_hook
def build_wheel(
wheel_directory: str,
config_settings: Optional[Dict[Any, Any]] = None,
Expand Down
6 changes: 6 additions & 0 deletions mesonpy/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
from typing import Union


if sys.version_info >= (3, 10):
from typing import ParamSpec
else:
from typing_extensions import ParamSpec

if sys.version_info >= (3, 9):
from collections.abc import (
Collection, Iterable, Iterator, Mapping, Sequence
Expand Down Expand Up @@ -46,5 +51,6 @@ def is_relative_to(path: pathlib.Path, other: Union[pathlib.Path, str]) -> bool:
'Literal',
'Mapping',
'Path',
'ParamSpec',
'Sequence',
]
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ requires = [
'meson>=0.63.3',
'pyproject-metadata>=0.6.1',
'tomli>=1.0.0; python_version<"3.11"',
'typing-extensions>=3.7.4; python_version<"3.8"',
'typing-extensions>=3.7.4; python_version<"3.10"',
]

[project]
Expand All @@ -28,7 +28,7 @@ dependencies = [
'meson>=0.63.3',
'pyproject-metadata>=0.6.1', # not a hard dependency, only needed for projects that use PEP 621 metadata
'tomli>=1.0.0; python_version<"3.11"',
'typing-extensions>=3.7.4; python_version<"3.8"',
'typing-extensions>=3.7.4; python_version<"3.10"',
]

dynamic = [
Expand Down
2 changes: 1 addition & 1 deletion tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def test_spam(venv, tmp_path):
with chdir(examples_dir / 'spam'):
if sys.version_info < (3, 8):
# The test project requires Python >= 3.8.
with pytest.raises(mesonpy.MesonBuilderError, match=r'Unsupported Python version `3.7.\d+`'):
with pytest.raises(SystemExit):
mesonpy.build_wheel(tmp_path)
else:
wheel = mesonpy.build_wheel(tmp_path)
Expand Down
41 changes: 19 additions & 22 deletions tests/test_pep517.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ def which(prog: str) -> bool:
# smoke check for the future if we add another usage
raise AssertionError(f'Called with {prog}, tests not expecting that usage')

subprocess_run = subprocess.run

def run(cmd: List[str], *args: object, **kwargs: object) -> subprocess.CompletedProcess:
if cmd != ['ninja', '--version']:
# smoke check for the future if we add another usage
raise AssertionError(f'Called with {cmd}, tests not expecting that usage')
return subprocess.CompletedProcess(cmd, 0, f'{ninja}\n', '')
if cmd == ['ninja', '--version']:
return subprocess.CompletedProcess(cmd, 0, f'{ninja}\n', '')
return subprocess_run(cmd, *args, **kwargs)

monkeypatch.setattr(shutil, 'which', which)
monkeypatch.setattr(subprocess, 'run', run)
Expand All @@ -55,23 +56,19 @@ def run(cmd: List[str], *args: object, **kwargs: object) -> subprocess.Completed
assert set(mesonpy.get_requires_for_build_wheel()) == expected


def test_invalid_config_settings(package_pure, tmp_path_session):
raises_error = pytest.raises(mesonpy.ConfigError, match="Unknown config setting: 'invalid'")

with raises_error:
mesonpy.build_sdist(tmp_path_session, {'invalid': ()})
with raises_error:
mesonpy.build_wheel(tmp_path_session, {'invalid': ()})


def test_invalid_config_settings_suggest(package_pure, tmp_path_session):
raises_error = pytest.raises(
mesonpy.ConfigError,
match='Did you mean one of: .*setup-args'
)
def test_invalid_config_settings(capsys, package_pure, tmp_path_session):
for method in mesonpy.build_sdist, mesonpy.build_wheel:
with pytest.raises(SystemExit):
method(tmp_path_session, {'invalid': ()})
out, err = capsys.readouterr()
assert out.splitlines()[-1].endswith(
'Unknown configuration entry "invalid"')

with raises_error:
mesonpy.build_sdist(tmp_path_session, {'setup_args': ()})

with raises_error:
mesonpy.build_wheel(tmp_path_session, {'setup_args': ()})
def test_invalid_config_settings_suggest(capsys, package_pure, tmp_path_session):
for method in mesonpy.build_sdist, mesonpy.build_wheel:
with pytest.raises(SystemExit):
method(tmp_path_session, {'setup_args': ()})
out, err = capsys.readouterr()
assert out.splitlines()[-1].endswith(
'Unknown configuration entry "setup_args". Did you mean "setup-args" or "dist-args"?')
5 changes: 2 additions & 3 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,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.MesonBuilderError, match='Unsupported dynamic 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`'
f'Unsupported Python version {platform.python_version()}, expected ==1.0.0'
)):
with mesonpy.Project.with_temp_working_dir():
pass
Expand Down