Skip to content

Commit

Permalink
ENH: implement variables substitution in configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
dnicolodi committed Sep 12, 2023
1 parent 1e79b26 commit 000ab53
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 1 deletion.
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ py.install_sources(
'mesonpy/_compat.py',
'mesonpy/_editable.py',
'mesonpy/_rpath.py',
'mesonpy/_substitutions.py',
'mesonpy/_tags.py',
'mesonpy/_util.py',
'mesonpy/_wheelfile.py',
Expand Down
7 changes: 6 additions & 1 deletion mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

import mesonpy._compat
import mesonpy._rpath
import mesonpy._substitutions
import mesonpy._tags
import mesonpy._util
import mesonpy._wheelfile
Expand Down Expand Up @@ -624,7 +625,11 @@ def __init__(
# load meson args from pyproject.toml
pyproject_config = _validate_pyproject_config(pyproject)
for key, value in pyproject_config.get('args', {}).items():
self._meson_args[key].extend(value)
try:
self._meson_args[key] = [mesonpy._substitutions.eval(x) for x in value]
except ValueError as exc:
raise ConfigError(
f'Cannot evaluate "tool.meson-python.args.{key}" configuration entry: {exc.args[0]}') from None

# meson arguments from the command line take precedence over
# arguments from the configuration file thus are added later
Expand Down
106 changes: 106 additions & 0 deletions mesonpy/_substitutions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# SPDX-FileCopyrightText: 2023 The meson-python developers
#
# SPDX-License-Identifier: MIT

from __future__ import annotations

import ast
import operator
import string
import sys
import typing


if typing.TYPE_CHECKING: # pragma: no cover
from typing import Any, Callable, Mapping, Optional, Type


_methods = {}


def _register(nodetype: Type[ast.AST]) -> Callable[..., Callable[..., Any]]:
def closure(method: Callable[[Interpreter, ast.AST], Any]) -> Callable[[Interpreter, ast.AST], Any]:
_methods[nodetype] = method
return method
return closure


class Interpreter:

_operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.FloorDiv: operator.floordiv,
}

def __init__(self, variables: Mapping[str, Any]):
self._variables = variables

def eval(self, string: str) -> Any:
try:
expr = ast.parse(string, mode='eval')
print(ast.dump(expr))
return self._eval(expr)
except KeyError as exc:
raise ValueError(f'unknown variable "{exc.args[0]}"') from exc
except NotImplementedError as exc:
raise ValueError(f'invalid expression {string!r}') from exc

__getitem__ = eval

def _eval(self, node: ast.AST) -> Any:
# Cannot use functools.singlemethoddispatch as long as Python 3.7 is supported.
method = _methods.get(type(node), None)
if method is None:
raise NotImplementedError

Check warning on line 57 in mesonpy/_substitutions.py

View check run for this annotation

Codecov / codecov/patch

mesonpy/_substitutions.py#L57

Added line #L57 was not covered by tests
return method(self, node)

@_register(ast.Expression)
def _expression(self, node: ast.Expression) -> Any:
return self._eval(node.body)

@_register(ast.BinOp)
def _binop(self, node: ast.BinOp) -> Any:
func = self._operators.get(type(node.op))
if func is None:
raise NotImplementedError
return func(self._eval(node.left), self._eval(node.right))

@_register(ast.Constant)
def _constant(self, node: ast.Constant) -> Any:
return node.value

if sys.version_info < (3, 8):

# Python 3.7, replaced by ast.Constant is later versions.
@_register(ast.Num)
def _num(self, node: ast.Num) -> Any:

Check warning on line 79 in mesonpy/_substitutions.py

View check run for this annotation

Codecov / codecov/patch

mesonpy/_substitutions.py#L79

Added line #L79 was not covered by tests
return node.n

# Python 3.7, replaced by ast.Constant is later versions.
@_register(ast.Str)
def _str(self, node: ast.Str) -> Any:
return node.s

Check warning on line 85 in mesonpy/_substitutions.py

View check run for this annotation

Codecov / codecov/patch

mesonpy/_substitutions.py#L84-L85

Added lines #L84 - L85 were not covered by tests

@_register(ast.Name)
def _variable(self, node: ast.Name) -> Any:
value = self._variables[node.id]
if callable(value):
value = value()
return value


def _ncores() -> int:
return 42

Check warning on line 96 in mesonpy/_substitutions.py

View check run for this annotation

Codecov / codecov/patch

mesonpy/_substitutions.py#L96

Added line #L96 was not covered by tests


class Template(string.Template):
braceidpattern = r'[^}]+'


def eval(template: str, variables: Optional[Mapping[str, Any]] = None) -> str:
if variables is None:
variables = {'ncores': _ncores}
return Template(template).substitute(Interpreter(variables))
5 changes: 5 additions & 0 deletions tests/packages/substitutions-invalid/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2023 The meson-python developers
#
# SPDX-License-Identifier: MIT

project('substitutions', version: '0.0.1')
10 changes: 10 additions & 0 deletions tests/packages/substitutions-invalid/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2023 The meson-python developers
#
# SPDX-License-Identifier: MIT

[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']

[tool.meson-python.args]
compile = ['-j', '$x']
5 changes: 5 additions & 0 deletions tests/packages/substitutions/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2023 The meson-python developers
#
# SPDX-License-Identifier: MIT

project('substitutions', version: '0.0.1')
10 changes: 10 additions & 0 deletions tests/packages/substitutions/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2023 The meson-python developers
#
# SPDX-License-Identifier: MIT

[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']

[tool.meson-python.args]
compile = ['-j', '${ncores // 2 + 2}']
38 changes: 38 additions & 0 deletions tests/test_substitutions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: 2023 The meson-python developers
#
# SPDX-License-Identifier: MIT

import pytest

import mesonpy


def test_interpolate_expression():
assert mesonpy._substitutions.eval('$x ${foo}', {'x': 1, 'foo': 2}) == '1 2'

Check warning on line 11 in tests/test_substitutions.py

View check run for this annotation

Codecov / codecov/patch

tests/test_substitutions.py#L11

Added line #L11 was not covered by tests


def test_interpolate_expression():
assert mesonpy._substitutions.eval('${(x + 2 * 3 - 1) // 3 / 2}', {'x': 1}) == '1.0'


def test_interpolate_key_error():
with pytest.raises(ValueError, match='unknown variable "y"'):
mesonpy._substitutions.eval('$y', {'x': 1})


def test_interpolate_not_implemented():
with pytest.raises(ValueError, match='invalid expression'):
mesonpy._substitutions.eval('${x ** 2}', {'x': 1})


def test_substitutions(package_substitutions, monkeypatch):
monkeypatch.setattr(mesonpy._substitutions, '_ncores', lambda: 2)
with mesonpy._project() as project:
assert project._meson_args['compile'] == ['-j', '3']


def test_substitutions_invalid(package_substitutions_invalid, monkeypatch):
monkeypatch.setattr(mesonpy._substitutions, '_ncores', lambda: 2)
with pytest.raises(mesonpy.ConfigError, match=''):
with mesonpy._project():
pass

0 comments on commit 000ab53

Please sign in to comment.