Skip to content

Commit 5fcf9e1

Browse files
committed
ENH: implement variables substitution in configuration
1 parent 71f5926 commit 5fcf9e1

File tree

8 files changed

+168
-0
lines changed

8 files changed

+168
-0
lines changed

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ py.install_sources(
1111
'mesonpy/_compat.py',
1212
'mesonpy/_editable.py',
1313
'mesonpy/_rpath.py',
14+
'mesonpy/_substitutions.py',
1415
'mesonpy/_tags.py',
1516
'mesonpy/_util.py',
1617
'mesonpy/_wheelfile.py',

mesonpy/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
import mesonpy._compat
4949
import mesonpy._rpath
50+
import mesonpy._substitutions
5051
import mesonpy._tags
5152
import mesonpy._util
5253
import mesonpy._wheelfile
@@ -660,6 +661,12 @@ def __init__( # noqa: C901
660661
# load meson args from pyproject.toml
661662
pyproject_config = _validate_pyproject_config(pyproject)
662663
for key, value in pyproject_config.get('args', {}).items():
664+
# apply variable interpolation
665+
try:
666+
value = [mesonpy._substitutions.interpolate(x) for x in value]
667+
except ValueError as exc:
668+
raise ConfigError(
669+
f'Cannot interpret value for "tool.meson-python.args.{key}" configuration entry: {exc.args[0]}') from None
663670
self._meson_args[key].extend(value)
664671

665672
# meson arguments from the command line take precedence over

mesonpy/_substitutions.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# SPDX-FileCopyrightText: 2023 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
from __future__ import annotations
6+
7+
import ast
8+
import operator
9+
import typing
10+
11+
12+
if typing.TYPE_CHECKING: # pragma: no cover
13+
from typing import Any, Callable, Mapping, Optional, Type
14+
15+
16+
_methods = {}
17+
18+
19+
def _register(nodetype: Type[ast.AST]) -> Callable[..., Callable[..., Any]]:
20+
def closure(method: Callable[[Interpreter, ast.AST], Any]) -> Callable[[Interpreter, ast.AST], Any]:
21+
_methods[nodetype] = method
22+
return method
23+
return closure
24+
25+
26+
class Interpreter:
27+
28+
_operators = {
29+
ast.Add: operator.add,
30+
ast.Sub: operator.sub,
31+
ast.Mult: operator.mul,
32+
ast.Div: operator.truediv,
33+
}
34+
35+
def __init__(self, variables: Mapping[str, Any]):
36+
self._variables = variables
37+
38+
def eval(self, string: str) -> Any:
39+
try:
40+
expr = ast.parse(string, mode='eval')
41+
return self._eval(expr)
42+
except KeyError as exc:
43+
raise ValueError(f'unknown variable "{exc.args[0]}"') from exc
44+
except NotImplementedError as exc:
45+
raise ValueError(f'invalid expression {string!r}') from exc
46+
47+
__getitem__ = eval
48+
49+
def _eval(self, node: ast.AST) -> Any:
50+
# Cannot use functools.singlemethoddispatch as long as Python 3.7 is supported.
51+
method = _methods.get(type(node), None)
52+
if method is None:
53+
raise NotImplementedError
54+
return method(self, node)
55+
56+
@_register(ast.Expression)
57+
def _expression(self, node: ast.Expression) -> Any:
58+
return self._eval(node.body)
59+
60+
@_register(ast.BinOp)
61+
def _binop(self, node: ast.BinOp) -> Any:
62+
func = self._operators.get(type(node.op))
63+
if func is None:
64+
raise NotImplementedError
65+
return func(self._eval(node.left), self._eval(node.right))
66+
67+
@_register(ast.Constant)
68+
def _constant(self, node: ast.Constant) -> Any:
69+
return node.value
70+
71+
# Python 3.7, replaced by ast.Constant is later versions.
72+
@_register(ast.Num)
73+
def _num(self, node: ast.Num) -> Any:
74+
return node.n
75+
76+
# Python 3.7, replaced by ast.Constant is later versions.
77+
@_register(ast.Str)
78+
def _str(self, node: ast.Str) -> Any:
79+
return node.s
80+
81+
@_register(ast.Name)
82+
def _variable(self, node: ast.Name) -> Any:
83+
value = self._variables[node.id]
84+
if callable(value):
85+
value = value()
86+
return value
87+
88+
89+
def _ncores() -> int:
90+
return 42
91+
92+
93+
def interpolate(string: str, variables: Optional[Mapping[str, Any]] = None) -> str:
94+
if variables is None:
95+
variables = {'ncores': _ncores}
96+
return string % Interpreter(variables)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SPDX-FileCopyrightText: 2023 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
project('substitutions', version: '0.0.1')
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-FileCopyrightText: 2023 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
[build-system]
6+
build-backend = 'mesonpy'
7+
requires = ['meson-python']
8+
9+
[tool.meson-python.args]
10+
compile = ['-j', '%(xxx)d']
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SPDX-FileCopyrightText: 2023 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
project('substitutions', version: '0.0.1')
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-FileCopyrightText: 2023 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
[build-system]
6+
build-backend = 'mesonpy'
7+
requires = ['meson-python']
8+
9+
[tool.meson-python.args]
10+
compile = ['-j', '%(ncores / 2 + 2)d']

tests/test_substitutions.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# SPDX-FileCopyrightText: 2023 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
import pytest
6+
7+
import mesonpy
8+
9+
10+
def test_interpolate():
11+
assert mesonpy._substitutions.interpolate('%(x * 2 + 3 - 4 / 1)d', {'x': 1}) == '1'
12+
13+
14+
def test_interpolate_key_error():
15+
with pytest.raises(RuntimeError, match='unknown variable "y"'):
16+
mesonpy._substitutions.interpolate('%(y)d', {'x': 1})
17+
18+
19+
def test_interpolate_not_implemented():
20+
with pytest.raises(RuntimeError, match='invalid expression'):
21+
mesonpy._substitutions.interpolate('%(x ** 2)d', {'x': 1})
22+
23+
24+
def test_substitutions(package_substitutions, monkeypatch):
25+
monkeypatch.setattr(mesonpy._substitutions, '_ncores', lambda: 2)
26+
with mesonpy._project() as project:
27+
assert project._meson_args['compile'] == ['-j', '3']
28+
29+
30+
def test_substitutions_invalid(package_substitutions_invalid, monkeypatch):
31+
monkeypatch.setattr(mesonpy._substitutions, '_ncores', lambda: 2)
32+
with pytest.raises(mesonpy.ConfigError, match=''):
33+
with mesonpy._project():
34+
pass

0 commit comments

Comments
 (0)