diff options
Diffstat (limited to '')
-rw-r--r-- | mesonbuild/interpreterbase/__init__.py | 135 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/_unholder.py | 35 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/baseobjects.py | 182 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/decorators.py | 791 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/disabler.py | 45 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/exceptions.py | 33 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/helpers.py | 56 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/interpreterbase.py | 604 | ||||
-rw-r--r-- | mesonbuild/interpreterbase/operator.py | 32 |
9 files changed, 1913 insertions, 0 deletions
diff --git a/mesonbuild/interpreterbase/__init__.py b/mesonbuild/interpreterbase/__init__.py new file mode 100644 index 0000000..13f55e5 --- /dev/null +++ b/mesonbuild/interpreterbase/__init__.py @@ -0,0 +1,135 @@ +# Copyright 2013-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ + 'InterpreterObject', + 'MesonInterpreterObject', + 'ObjectHolder', + 'IterableObject', + 'MutableInterpreterObject', + + 'MesonOperator', + + 'Disabler', + 'is_disabled', + + 'InterpreterException', + 'InvalidCode', + 'InvalidArguments', + 'SubdirDoneRequest', + 'ContinueRequest', + 'BreakRequest', + + 'default_resolve_key', + 'flatten', + 'resolve_second_level_holders', + + 'noPosargs', + 'noKwargs', + 'stringArgs', + 'noArgsFlattening', + 'noSecondLevelHolderResolving', + 'unholder_return', + 'disablerIfNotFound', + 'permittedKwargs', + 'typed_operator', + 'unary_operator', + 'typed_pos_args', + 'ContainerTypeInfo', + 'KwargInfo', + 'typed_kwargs', + 'FeatureCheckBase', + 'FeatureNew', + 'FeatureDeprecated', + 'FeatureNewKwargs', + 'FeatureDeprecatedKwargs', + + 'InterpreterBase', + + 'SubProject', + + 'TV_fw_var', + 'TV_fw_args', + 'TV_fw_kwargs', + 'TV_func', + 'TYPE_elementary', + 'TYPE_var', + 'TYPE_nvar', + 'TYPE_kwargs', + 'TYPE_nkwargs', + 'TYPE_key_resolver', + 'TYPE_HoldableTypes', + + 'HoldableTypes', +] + +from .baseobjects import ( + InterpreterObject, + MesonInterpreterObject, + ObjectHolder, + IterableObject, + MutableInterpreterObject, + + TV_fw_var, + TV_fw_args, + TV_fw_kwargs, + TV_func, + TYPE_elementary, + TYPE_var, + TYPE_nvar, + TYPE_kwargs, + TYPE_nkwargs, + TYPE_key_resolver, + TYPE_HoldableTypes, + + SubProject, + + HoldableTypes, +) + +from .decorators import ( + noPosargs, + noKwargs, + stringArgs, + noArgsFlattening, + noSecondLevelHolderResolving, + unholder_return, + disablerIfNotFound, + permittedKwargs, + typed_pos_args, + ContainerTypeInfo, + KwargInfo, + typed_operator, + unary_operator, + typed_kwargs, + FeatureCheckBase, + FeatureNew, + FeatureDeprecated, + FeatureNewKwargs, + FeatureDeprecatedKwargs, +) + +from .exceptions import ( + InterpreterException, + InvalidCode, + InvalidArguments, + SubdirDoneRequest, + ContinueRequest, + BreakRequest, +) + +from .disabler import Disabler, is_disabled +from .helpers import default_resolve_key, flatten, resolve_second_level_holders +from .interpreterbase import InterpreterBase +from .operator import MesonOperator diff --git a/mesonbuild/interpreterbase/_unholder.py b/mesonbuild/interpreterbase/_unholder.py new file mode 100644 index 0000000..4f1edc1 --- /dev/null +++ b/mesonbuild/interpreterbase/_unholder.py @@ -0,0 +1,35 @@ +# Copyright 2013-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import typing as T + +from .baseobjects import InterpreterObject, MesonInterpreterObject, ObjectHolder, HoldableTypes +from .exceptions import InvalidArguments +from ..mesonlib import HoldableObject, MesonBugException + +if T.TYPE_CHECKING: + from .baseobjects import TYPE_var + +def _unholder(obj: InterpreterObject) -> TYPE_var: + if isinstance(obj, ObjectHolder): + assert isinstance(obj.held_object, HoldableTypes) + return obj.held_object + elif isinstance(obj, MesonInterpreterObject): + return obj + elif isinstance(obj, HoldableObject): + raise MesonBugException(f'Argument {obj} of type {type(obj).__name__} is not held by an ObjectHolder.') + elif isinstance(obj, InterpreterObject): + raise InvalidArguments(f'Argument {obj} of type {type(obj).__name__} cannot be passed to a method or function') + raise MesonBugException(f'Unknown object {obj} of type {type(obj).__name__} in the parameters.') diff --git a/mesonbuild/interpreterbase/baseobjects.py b/mesonbuild/interpreterbase/baseobjects.py new file mode 100644 index 0000000..820e091 --- /dev/null +++ b/mesonbuild/interpreterbase/baseobjects.py @@ -0,0 +1,182 @@ +# Copyright 2013-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from .. import mparser +from .exceptions import InvalidCode, InvalidArguments +from .helpers import flatten, resolve_second_level_holders +from .operator import MesonOperator +from ..mesonlib import HoldableObject, MesonBugException +import textwrap + +import typing as T +from abc import ABCMeta + +if T.TYPE_CHECKING: + from typing_extensions import Protocol + + # Object holders need the actual interpreter + from ..interpreter import Interpreter + + __T = T.TypeVar('__T', bound='TYPE_var', contravariant=True) + + class OperatorCall(Protocol[__T]): + def __call__(self, other: __T) -> 'TYPE_var': ... + +TV_fw_var = T.Union[str, int, bool, list, dict, 'InterpreterObject'] +TV_fw_args = T.List[T.Union[mparser.BaseNode, TV_fw_var]] +TV_fw_kwargs = T.Dict[str, T.Union[mparser.BaseNode, TV_fw_var]] + +TV_func = T.TypeVar('TV_func', bound=T.Callable[..., T.Any]) + +TYPE_elementary = T.Union[str, int, bool, T.List[T.Any], T.Dict[str, T.Any]] +TYPE_var = T.Union[TYPE_elementary, HoldableObject, 'MesonInterpreterObject'] +TYPE_nvar = T.Union[TYPE_var, mparser.BaseNode] +TYPE_kwargs = T.Dict[str, TYPE_var] +TYPE_nkwargs = T.Dict[str, TYPE_nvar] +TYPE_key_resolver = T.Callable[[mparser.BaseNode], str] + +SubProject = T.NewType('SubProject', str) + +class InterpreterObject: + def __init__(self, *, subproject: T.Optional['SubProject'] = None) -> None: + self.methods: T.Dict[ + str, + T.Callable[[T.List[TYPE_var], TYPE_kwargs], TYPE_var] + ] = {} + self.operators: T.Dict[MesonOperator, 'OperatorCall'] = {} + self.trivial_operators: T.Dict[ + MesonOperator, + T.Tuple[ + T.Union[T.Type, T.Tuple[T.Type, ...]], + 'OperatorCall' + ] + ] = {} + # Current node set during a method call. This can be used as location + # when printing a warning message during a method call. + self.current_node: mparser.BaseNode = None + self.subproject = subproject or SubProject('') + + # Some default operators supported by all objects + self.operators.update({ + MesonOperator.EQUALS: self.op_equals, + MesonOperator.NOT_EQUALS: self.op_not_equals, + }) + + # The type of the object that can be printed to the user + def display_name(self) -> str: + return type(self).__name__ + + def method_call( + self, + method_name: str, + args: T.List[TYPE_var], + kwargs: TYPE_kwargs + ) -> TYPE_var: + if method_name in self.methods: + method = self.methods[method_name] + if not getattr(method, 'no-args-flattening', False): + args = flatten(args) + if not getattr(method, 'no-second-level-holder-flattening', False): + args, kwargs = resolve_second_level_holders(args, kwargs) + return method(args, kwargs) + raise InvalidCode(f'Unknown method "{method_name}" in object {self} of type {type(self).__name__}.') + + def operator_call(self, operator: MesonOperator, other: TYPE_var) -> TYPE_var: + if operator in self.trivial_operators: + op = self.trivial_operators[operator] + if op[0] is None and other is not None: + raise MesonBugException(f'The unary operator `{operator.value}` of {self.display_name()} was passed the object {other} of type {type(other).__name__}') + if op[0] is not None and not isinstance(other, op[0]): + raise InvalidArguments(f'The `{operator.value}` operator of {self.display_name()} does not accept objects of type {type(other).__name__} ({other})') + return op[1](other) + if operator in self.operators: + return self.operators[operator](other) + raise InvalidCode(f'Object {self} of type {self.display_name()} does not support the `{operator.value}` operator.') + + # Default comparison operator support + def _throw_comp_exception(self, other: TYPE_var, opt_type: str) -> T.NoReturn: + raise InvalidArguments(textwrap.dedent( + f''' + Trying to compare values of different types ({self.display_name()}, {type(other).__name__}) using {opt_type}. + This was deprecated and undefined behavior previously and is as of 0.60.0 a hard error. + ''' + )) + + def op_equals(self, other: TYPE_var) -> bool: + # We use `type(...) == type(...)` here to enforce an *exact* match for comparison. We + # don't want comparisons to be possible where `isinstance(derived_obj, type(base_obj))` + # would pass because this comparison must never be true: `derived_obj == base_obj` + if type(self) != type(other): + self._throw_comp_exception(other, '==') + return self == other + + def op_not_equals(self, other: TYPE_var) -> bool: + if type(self) != type(other): + self._throw_comp_exception(other, '!=') + return self != other + +class MesonInterpreterObject(InterpreterObject): + ''' All non-elementary objects and non-object-holders should be derived from this ''' + +class MutableInterpreterObject: + ''' Dummy class to mark the object type as mutable ''' + +HoldableTypes = (HoldableObject, int, bool, str, list, dict) +TYPE_HoldableTypes = T.Union[TYPE_elementary, HoldableObject] +InterpreterObjectTypeVar = T.TypeVar('InterpreterObjectTypeVar', bound=TYPE_HoldableTypes) + +class ObjectHolder(InterpreterObject, T.Generic[InterpreterObjectTypeVar]): + def __init__(self, obj: InterpreterObjectTypeVar, interpreter: 'Interpreter') -> None: + super().__init__(subproject=interpreter.subproject) + # This causes some type checkers to assume that obj is a base + # HoldableObject, not the specialized type, so only do this assert in + # non-type checking situations + if not T.TYPE_CHECKING: + assert isinstance(obj, HoldableTypes), f'This is a bug: Trying to hold object of type `{type(obj).__name__}` that is not in `{HoldableTypes}`' + self.held_object = obj + self.interpreter = interpreter + self.env = self.interpreter.environment + + # Hide the object holder abstraction from the user + def display_name(self) -> str: + return type(self.held_object).__name__ + + # Override default comparison operators for the held object + def op_equals(self, other: TYPE_var) -> bool: + # See the comment from InterpreterObject why we are using `type()` here. + if type(self.held_object) != type(other): + self._throw_comp_exception(other, '==') + return self.held_object == other + + def op_not_equals(self, other: TYPE_var) -> bool: + if type(self.held_object) != type(other): + self._throw_comp_exception(other, '!=') + return self.held_object != other + + def __repr__(self) -> str: + return f'<[{type(self).__name__}] holds [{type(self.held_object).__name__}]: {self.held_object!r}>' + +class IterableObject(metaclass=ABCMeta): + '''Base class for all objects that can be iterated over in a foreach loop''' + + def iter_tuple_size(self) -> T.Optional[int]: + '''Return the size of the tuple for each iteration. Returns None if only a single value is returned.''' + raise MesonBugException(f'iter_tuple_size not implemented for {self.__class__.__name__}') + + def iter_self(self) -> T.Iterator[T.Union[TYPE_var, T.Tuple[TYPE_var, ...]]]: + raise MesonBugException(f'iter not implemented for {self.__class__.__name__}') + + def size(self) -> int: + raise MesonBugException(f'size not implemented for {self.__class__.__name__}') diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py new file mode 100644 index 0000000..173cedc --- /dev/null +++ b/mesonbuild/interpreterbase/decorators.py @@ -0,0 +1,791 @@ +# Copyright 2013-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from .. import mesonlib, mlog +from .disabler import Disabler +from .exceptions import InterpreterException, InvalidArguments +from ._unholder import _unholder + +from dataclasses import dataclass +from functools import wraps +import abc +import itertools +import copy +import typing as T + +if T.TYPE_CHECKING: + from typing_extensions import Protocol + + from .. import mparser + from .baseobjects import InterpreterObject, TV_func, TYPE_var, TYPE_kwargs + from .interpreterbase import SubProject + from .operator import MesonOperator + + _TV_IntegerObject = T.TypeVar('_TV_IntegerObject', bound=InterpreterObject, contravariant=True) + _TV_ARG1 = T.TypeVar('_TV_ARG1', bound=TYPE_var, contravariant=True) + + class FN_Operator(Protocol[_TV_IntegerObject, _TV_ARG1]): + def __call__(s, self: _TV_IntegerObject, other: _TV_ARG1) -> TYPE_var: ... + _TV_FN_Operator = T.TypeVar('_TV_FN_Operator', bound=FN_Operator) + +def get_callee_args(wrapped_args: T.Sequence[T.Any]) -> T.Tuple['mparser.BaseNode', T.List['TYPE_var'], 'TYPE_kwargs', 'SubProject']: + # First argument could be InterpreterBase, InterpreterObject or ModuleObject. + # In the case of a ModuleObject it is the 2nd argument (ModuleState) that + # contains the needed information. + s = wrapped_args[0] + if not hasattr(s, 'current_node'): + s = wrapped_args[1] + node = s.current_node + subproject = s.subproject + args = kwargs = None + if len(wrapped_args) >= 3: + args = wrapped_args[-2] + kwargs = wrapped_args[-1] + return node, args, kwargs, subproject + +def noPosargs(f: TV_func) -> TV_func: + @wraps(f) + def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + args = get_callee_args(wrapped_args)[1] + if args: + raise InvalidArguments('Function does not take positional arguments.') + return f(*wrapped_args, **wrapped_kwargs) + return T.cast('TV_func', wrapped) + +def noKwargs(f: TV_func) -> TV_func: + @wraps(f) + def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + kwargs = get_callee_args(wrapped_args)[2] + if kwargs: + raise InvalidArguments('Function does not take keyword arguments.') + return f(*wrapped_args, **wrapped_kwargs) + return T.cast('TV_func', wrapped) + +def stringArgs(f: TV_func) -> TV_func: + @wraps(f) + def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + args = get_callee_args(wrapped_args)[1] + if not isinstance(args, list): + mlog.debug('Not a list:', str(args)) + raise InvalidArguments('Argument not a list.') + if not all(isinstance(s, str) for s in args): + mlog.debug('Element not a string:', str(args)) + raise InvalidArguments('Arguments must be strings.') + return f(*wrapped_args, **wrapped_kwargs) + return T.cast('TV_func', wrapped) + +def noArgsFlattening(f: TV_func) -> TV_func: + setattr(f, 'no-args-flattening', True) # noqa: B010 + return f + +def noSecondLevelHolderResolving(f: TV_func) -> TV_func: + setattr(f, 'no-second-level-holder-flattening', True) # noqa: B010 + return f + +def unholder_return(f: TV_func) -> T.Callable[..., TYPE_var]: + @wraps(f) + def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + res = f(*wrapped_args, **wrapped_kwargs) + return _unholder(res) + return T.cast('T.Callable[..., TYPE_var]', wrapped) + +def disablerIfNotFound(f: TV_func) -> TV_func: + @wraps(f) + def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + kwargs = get_callee_args(wrapped_args)[2] + disabler = kwargs.pop('disabler', False) + ret = f(*wrapped_args, **wrapped_kwargs) + if disabler and not ret.found(): + return Disabler() + return ret + return T.cast('TV_func', wrapped) + +@dataclass(repr=False, eq=False) +class permittedKwargs: + permitted: T.Set[str] + + def __call__(self, f: TV_func) -> TV_func: + @wraps(f) + def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + kwargs = get_callee_args(wrapped_args)[2] + unknowns = set(kwargs).difference(self.permitted) + if unknowns: + ustr = ', '.join([f'"{u}"' for u in sorted(unknowns)]) + raise InvalidArguments(f'Got unknown keyword arguments {ustr}') + return f(*wrapped_args, **wrapped_kwargs) + return T.cast('TV_func', wrapped) + +def typed_operator(operator: MesonOperator, + types: T.Union[T.Type, T.Tuple[T.Type, ...]]) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']: + """Decorator that does type checking for operator calls. + + The principle here is similar to typed_pos_args, however much simpler + since only one other object ever is passed + """ + def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator': + @wraps(f) + def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var: + if not isinstance(other, types): + raise InvalidArguments(f'The `{operator.value}` of {self.display_name()} does not accept objects of type {type(other).__name__} ({other})') + return f(self, other) + return T.cast('_TV_FN_Operator', wrapper) + return inner + +def unary_operator(operator: MesonOperator) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']: + """Decorator that does type checking for unary operator calls. + + This decorator is for unary operators that do not take any other objects. + It should be impossible for a user to accidentally break this. Triggering + this check always indicates a bug in the Meson interpreter. + """ + def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator': + @wraps(f) + def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var: + if other is not None: + raise mesonlib.MesonBugException(f'The unary operator `{operator.value}` of {self.display_name()} was passed the object {other} of type {type(other).__name__}') + return f(self, other) + return T.cast('_TV_FN_Operator', wrapper) + return inner + + +def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]], + varargs: T.Optional[T.Union[T.Type, T.Tuple[T.Type, ...]]] = None, + optargs: T.Optional[T.List[T.Union[T.Type, T.Tuple[T.Type, ...]]]] = None, + min_varargs: int = 0, max_varargs: int = 0) -> T.Callable[..., T.Any]: + """Decorator that types type checking of positional arguments. + + This supports two different models of optional arguments, the first is the + variadic argument model. Variadic arguments are a possibly bounded, + possibly unbounded number of arguments of the same type (unions are + supported). The second is the standard default value model, in this case + a number of optional arguments may be provided, but they are still + ordered, and they may have different types. + + This function does not support mixing variadic and default arguments. + + :name: The name of the decorated function (as displayed in error messages) + :varargs: They type(s) of any variadic arguments the function takes. If + None the function takes no variadic args + :min_varargs: the minimum number of variadic arguments taken + :max_varargs: the maximum number of variadic arguments taken. 0 means unlimited + :optargs: The types of any optional arguments parameters taken. If None + then no optional parameters are taken. + + Some examples of usage blow: + >>> @typed_pos_args('mod.func', str, (str, int)) + ... def func(self, state: ModuleState, args: T.Tuple[str, T.Union[str, int]], kwargs: T.Dict[str, T.Any]) -> T.Any: + ... pass + + >>> @typed_pos_args('method', str, varargs=str) + ... def method(self, node: BaseNode, args: T.Tuple[str, T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any: + ... pass + + >>> @typed_pos_args('method', varargs=str, min_varargs=1) + ... def method(self, node: BaseNode, args: T.Tuple[T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any: + ... pass + + >>> @typed_pos_args('method', str, optargs=[(str, int), str]) + ... def method(self, node: BaseNode, args: T.Tuple[str, T.Optional[T.Union[str, int]], T.Optional[str]], kwargs: T.Dict[str, T.Any]) -> T.Any: + ... pass + + When should you chose `typed_pos_args('name', varargs=str, + min_varargs=1)` vs `typed_pos_args('name', str, varargs=str)`? + + The answer has to do with the semantics of the function, if all of the + inputs are the same type (such as with `files()`) then the former is + correct, all of the arguments are string names of files. If the first + argument is something else the it should be separated. + """ + def inner(f: TV_func) -> TV_func: + + @wraps(f) + def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + args = get_callee_args(wrapped_args)[1] + + # These are implementation programming errors, end users should never see them. + assert isinstance(args, list), args + assert max_varargs >= 0, 'max_varags cannot be negative' + assert min_varargs >= 0, 'min_varags cannot be negative' + assert optargs is None or varargs is None, \ + 'varargs and optargs not supported together as this would be ambiguous' + + num_args = len(args) + num_types = len(types) + a_types = types + + if varargs: + min_args = num_types + min_varargs + max_args = num_types + max_varargs + if max_varargs == 0 and num_args < min_args: + raise InvalidArguments(f'{name} takes at least {min_args} arguments, but got {num_args}.') + elif max_varargs != 0 and (num_args < min_args or num_args > max_args): + raise InvalidArguments(f'{name} takes between {min_args} and {max_args} arguments, but got {num_args}.') + elif optargs: + if num_args < num_types: + raise InvalidArguments(f'{name} takes at least {num_types} arguments, but got {num_args}.') + elif num_args > num_types + len(optargs): + raise InvalidArguments(f'{name} takes at most {num_types + len(optargs)} arguments, but got {num_args}.') + # Add the number of positional arguments required + if num_args > num_types: + diff = num_args - num_types + a_types = tuple(list(types) + list(optargs[:diff])) + elif num_args != num_types: + raise InvalidArguments(f'{name} takes exactly {num_types} arguments, but got {num_args}.') + + for i, (arg, type_) in enumerate(itertools.zip_longest(args, a_types, fillvalue=varargs), start=1): + if not isinstance(arg, type_): + if isinstance(type_, tuple): + shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in type_)) + else: + shouldbe = f'"{type_.__name__}"' + raise InvalidArguments(f'{name} argument {i} was of type "{type(arg).__name__}" but should have been {shouldbe}') + + # Ensure that we're actually passing a tuple. + # Depending on what kind of function we're calling the length of + # wrapped_args can vary. + nargs = list(wrapped_args) + i = nargs.index(args) + if varargs: + # if we have varargs we need to split them into a separate + # tuple, as python's typing doesn't understand tuples with + # fixed elements and variadic elements, only one or the other. + # so in that case we need T.Tuple[int, str, float, T.Tuple[str, ...]] + pos = args[:len(types)] + var = list(args[len(types):]) + pos.append(var) + nargs[i] = tuple(pos) + elif optargs: + if num_args < num_types + len(optargs): + diff = num_types + len(optargs) - num_args + nargs[i] = tuple(list(args) + [None] * diff) + else: + nargs[i] = args + else: + nargs[i] = tuple(args) + return f(*nargs, **wrapped_kwargs) + + return T.cast('TV_func', wrapper) + return inner + + +class ContainerTypeInfo: + + """Container information for keyword arguments. + + For keyword arguments that are containers (list or dict), this class encodes + that information. + + :param container: the type of container + :param contains: the types the container holds + :param pairs: if the container is supposed to be of even length. + This is mainly used for interfaces that predate the addition of dictionaries, and use + `[key, value, key2, value2]` format. + :param allow_empty: Whether this container is allowed to be empty + There are some cases where containers not only must be passed, but must + not be empty, and other cases where an empty container is allowed. + """ + + def __init__(self, container: T.Type, contains: T.Union[T.Type, T.Tuple[T.Type, ...]], *, + pairs: bool = False, allow_empty: bool = True): + self.container = container + self.contains = contains + self.pairs = pairs + self.allow_empty = allow_empty + + def check(self, value: T.Any) -> bool: + """Check that a value is valid. + + :param value: A value to check + :return: True if it is valid, False otherwise + """ + if not isinstance(value, self.container): + return False + iter_ = iter(value.values()) if isinstance(value, dict) else iter(value) + for each in iter_: + if not isinstance(each, self.contains): + return False + if self.pairs and len(value) % 2 != 0: + return False + if not value and not self.allow_empty: + return False + return True + + def description(self) -> str: + """Human readable description of this container type. + + :return: string to be printed + """ + container = 'dict' if self.container is dict else 'array' + if isinstance(self.contains, tuple): + contains = ' | '.join([t.__name__ for t in self.contains]) + else: + contains = self.contains.__name__ + s = f'{container}[{contains}]' + if self.pairs: + s += ' that has even size' + if not self.allow_empty: + s += ' that cannot be empty' + return s + +_T = T.TypeVar('_T') + +class _NULL_T: + """Special null type for evolution, this is an implementation detail.""" + + +_NULL = _NULL_T() + +class KwargInfo(T.Generic[_T]): + + """A description of a keyword argument to a meson function + + This is used to describe a value to the :func:typed_kwargs function. + + :param name: the name of the parameter + :param types: A type or tuple of types that are allowed, or a :class:ContainerType + :param required: Whether this is a required keyword argument. defaults to False + :param listify: If true, then the argument will be listified before being + checked. This is useful for cases where the Meson DSL allows a scalar or + a container, but internally we only want to work with containers + :param default: A default value to use if this isn't set. defaults to None, + this may be safely set to a mutable type, as long as that type does not + itself contain mutable types, typed_kwargs will copy the default + :param since: Meson version in which this argument has been added. defaults to None + :param since_message: An extra message to pass to FeatureNew when since is triggered + :param deprecated: Meson version in which this argument has been deprecated. defaults to None + :param deprecated_message: An extra message to pass to FeatureDeprecated + when since is triggered + :param validator: A callable that does additional validation. This is mainly + intended for cases where a string is expected, but only a few specific + values are accepted. Must return None if the input is valid, or a + message if the input is invalid + :param convertor: A callable that converts the raw input value into a + different type. This is intended for cases such as the meson DSL using a + string, but the implementation using an Enum. This should not do + validation, just conversion. + :param deprecated_values: a dictionary mapping a value to the version of + meson it was deprecated in. The Value may be any valid value for this + argument. + :param since_values: a dictionary mapping a value to the version of meson it was + added in. + :param not_set_warning: A warning message that is logged if the kwarg is not + set by the user. + :param feature_validator: A callable returning an iterable of FeatureNew | FeatureDeprecated objects. + """ + def __init__(self, name: str, + types: T.Union[T.Type[_T], T.Tuple[T.Union[T.Type[_T], ContainerTypeInfo], ...], ContainerTypeInfo], + *, required: bool = False, listify: bool = False, + default: T.Optional[_T] = None, + since: T.Optional[str] = None, + since_message: T.Optional[str] = None, + since_values: T.Optional[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]]] = None, + deprecated: T.Optional[str] = None, + deprecated_message: T.Optional[str] = None, + deprecated_values: T.Optional[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]]] = None, + feature_validator: T.Optional[T.Callable[[_T], T.Iterable[FeatureCheckBase]]] = None, + validator: T.Optional[T.Callable[[T.Any], T.Optional[str]]] = None, + convertor: T.Optional[T.Callable[[_T], object]] = None, + not_set_warning: T.Optional[str] = None): + self.name = name + self.types = types + self.required = required + self.listify = listify + self.default = default + self.since = since + self.since_message = since_message + self.since_values = since_values + self.feature_validator = feature_validator + self.deprecated = deprecated + self.deprecated_message = deprecated_message + self.deprecated_values = deprecated_values + self.validator = validator + self.convertor = convertor + self.not_set_warning = not_set_warning + + def evolve(self, *, + name: T.Union[str, _NULL_T] = _NULL, + required: T.Union[bool, _NULL_T] = _NULL, + listify: T.Union[bool, _NULL_T] = _NULL, + default: T.Union[_T, None, _NULL_T] = _NULL, + since: T.Union[str, None, _NULL_T] = _NULL, + since_message: T.Union[str, None, _NULL_T] = _NULL, + since_values: T.Union[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]], None, _NULL_T] = _NULL, + deprecated: T.Union[str, None, _NULL_T] = _NULL, + deprecated_message: T.Union[str, None, _NULL_T] = _NULL, + deprecated_values: T.Union[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]], None, _NULL_T] = _NULL, + feature_validator: T.Union[T.Callable[[_T], T.Iterable[FeatureCheckBase]], None, _NULL_T] = _NULL, + validator: T.Union[T.Callable[[_T], T.Optional[str]], None, _NULL_T] = _NULL, + convertor: T.Union[T.Callable[[_T], TYPE_var], None, _NULL_T] = _NULL) -> 'KwargInfo': + """Create a shallow copy of this KwargInfo, with modifications. + + This allows us to create a new copy of a KwargInfo with modifications. + This allows us to use a shared kwarg that implements complex logic, but + has slight differences in usage, such as being added to different + functions in different versions of Meson. + + The use the _NULL special value here allows us to pass None, which has + meaning in many of these cases. _NULL itself is never stored, always + being replaced by either the copy in self, or the provided new version. + """ + return type(self)( + name if not isinstance(name, _NULL_T) else self.name, + self.types, + listify=listify if not isinstance(listify, _NULL_T) else self.listify, + required=required if not isinstance(required, _NULL_T) else self.required, + default=default if not isinstance(default, _NULL_T) else self.default, + since=since if not isinstance(since, _NULL_T) else self.since, + since_message=since_message if not isinstance(since_message, _NULL_T) else self.since_message, + since_values=since_values if not isinstance(since_values, _NULL_T) else self.since_values, + deprecated=deprecated if not isinstance(deprecated, _NULL_T) else self.deprecated, + deprecated_message=deprecated_message if not isinstance(deprecated_message, _NULL_T) else self.deprecated_message, + deprecated_values=deprecated_values if not isinstance(deprecated_values, _NULL_T) else self.deprecated_values, + feature_validator=feature_validator if not isinstance(feature_validator, _NULL_T) else self.feature_validator, + validator=validator if not isinstance(validator, _NULL_T) else self.validator, + convertor=convertor if not isinstance(convertor, _NULL_T) else self.convertor, + ) + + +def typed_kwargs(name: str, *types: KwargInfo, allow_unknown: bool = False) -> T.Callable[..., T.Any]: + """Decorator for type checking keyword arguments. + + Used to wrap a meson DSL implementation function, where it checks various + things about keyword arguments, including the type, and various other + information. For non-required values it sets the value to a default, which + means the value will always be provided. + + If type tyhpe is a :class:ContainerTypeInfo, then the default value will be + passed as an argument to the container initializer, making a shallow copy + + :param name: the name of the function, including the object it's attached to + (if applicable) + :param *types: KwargInfo entries for each keyword argument. + """ + def inner(f: TV_func) -> TV_func: + + def types_description(types_tuple: T.Tuple[T.Union[T.Type, ContainerTypeInfo], ...]) -> str: + candidates = [] + for t in types_tuple: + if isinstance(t, ContainerTypeInfo): + candidates.append(t.description()) + else: + candidates.append(t.__name__) + shouldbe = 'one of: ' if len(candidates) > 1 else '' + shouldbe += ', '.join(candidates) + return shouldbe + + def raw_description(t: object) -> str: + """describe a raw type (ie, one that is not a ContainerTypeInfo).""" + if isinstance(t, list): + if t: + return f"array[{' | '.join(sorted(mesonlib.OrderedSet(type(v).__name__ for v in t)))}]" + return 'array[]' + elif isinstance(t, dict): + if t: + return f"dict[{' | '.join(sorted(mesonlib.OrderedSet(type(v).__name__ for v in t.values())))}]" + return 'dict[]' + return type(t).__name__ + + def check_value_type(types_tuple: T.Tuple[T.Union[T.Type, ContainerTypeInfo], ...], + value: T.Any) -> bool: + for t in types_tuple: + if isinstance(t, ContainerTypeInfo): + if t.check(value): + return True + elif isinstance(value, t): + return True + return False + + @wraps(f) + def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + + def emit_feature_change(values: T.Dict[_T, T.Union[str, T.Tuple[str, str]]], feature: T.Union[T.Type['FeatureDeprecated'], T.Type['FeatureNew']]) -> None: + for n, version in values.items(): + warn = False + if isinstance(version, tuple): + version, msg = version + else: + msg = None + + if n in {dict, list}: + assert isinstance(n, type), 'for mypy' + if isinstance(value, n): + feature.single_use(f'"{name}" keyword argument "{info.name}" of type {n.__name__}', version, subproject, msg, location=node) + elif isinstance(value, (dict, list)): + warn = n in value + else: + warn = n == value + + if warn: + feature.single_use(f'"{name}" keyword argument "{info.name}" value "{n}"', version, subproject, msg, location=node) + + node, _, _kwargs, subproject = get_callee_args(wrapped_args) + # Cast here, as the convertor function may place something other than a TYPE_var in the kwargs + kwargs = T.cast('T.Dict[str, object]', _kwargs) + + if not allow_unknown: + all_names = {t.name for t in types} + unknowns = set(kwargs).difference(all_names) + if unknowns: + ustr = ', '.join([f'"{u}"' for u in sorted(unknowns)]) + raise InvalidArguments(f'{name} got unknown keyword arguments {ustr}') + + for info in types: + types_tuple = info.types if isinstance(info.types, tuple) else (info.types,) + value = kwargs.get(info.name) + if value is not None: + if info.since: + feature_name = info.name + ' arg in ' + name + FeatureNew.single_use(feature_name, info.since, subproject, info.since_message, location=node) + if info.deprecated: + feature_name = info.name + ' arg in ' + name + FeatureDeprecated.single_use(feature_name, info.deprecated, subproject, info.deprecated_message, location=node) + if info.listify: + kwargs[info.name] = value = mesonlib.listify(value) + if not check_value_type(types_tuple, value): + shouldbe = types_description(types_tuple) + raise InvalidArguments(f'{name} keyword argument {info.name!r} was of type {raw_description(value)} but should have been {shouldbe}') + + if info.validator is not None: + msg = info.validator(value) + if msg is not None: + raise InvalidArguments(f'{name} keyword argument "{info.name}" {msg}') + + if info.feature_validator is not None: + for each in info.feature_validator(value): + each.use(subproject, node) + + if info.deprecated_values is not None: + emit_feature_change(info.deprecated_values, FeatureDeprecated) + + if info.since_values is not None: + emit_feature_change(info.since_values, FeatureNew) + + elif info.required: + raise InvalidArguments(f'{name} is missing required keyword argument "{info.name}"') + else: + # set the value to the default, this ensuring all kwargs are present + # This both simplifies the typing checking and the usage + assert check_value_type(types_tuple, info.default), f'In funcion {name} default value of {info.name} is not a valid type, got {type(info.default)} expected {types_description(types_tuple)}' + # Create a shallow copy of the container. This allows mutable + # types to be used safely as default values + kwargs[info.name] = copy.copy(info.default) + if info.not_set_warning: + mlog.warning(info.not_set_warning) + + if info.convertor: + kwargs[info.name] = info.convertor(kwargs[info.name]) + + return f(*wrapped_args, **wrapped_kwargs) + return T.cast('TV_func', wrapper) + return inner + + +# This cannot be a dataclass due to https://github.com/python/mypy/issues/5374 +class FeatureCheckBase(metaclass=abc.ABCMeta): + "Base class for feature version checks" + + feature_registry: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[T.Tuple[str, T.Optional['mparser.BaseNode']]]]]] + emit_notice = False + + def __init__(self, feature_name: str, feature_version: str, extra_message: str = ''): + self.feature_name = feature_name # type: str + self.feature_version = feature_version # type: str + self.extra_message = extra_message # type: str + + @staticmethod + def get_target_version(subproject: str) -> str: + # Don't do any checks if project() has not been parsed yet + if subproject not in mesonlib.project_meson_versions: + return '' + return mesonlib.project_meson_versions[subproject] + + @staticmethod + @abc.abstractmethod + def check_version(target_version: str, feature_version: str) -> bool: + pass + + def use(self, subproject: 'SubProject', location: T.Optional['mparser.BaseNode'] = None) -> None: + tv = self.get_target_version(subproject) + # No target version + if tv == '': + return + # Target version is new enough, don't warn + if self.check_version(tv, self.feature_version) and not self.emit_notice: + return + # Feature is too new for target version or we want to emit notices, register it + if subproject not in self.feature_registry: + self.feature_registry[subproject] = {self.feature_version: set()} + register = self.feature_registry[subproject] + if self.feature_version not in register: + register[self.feature_version] = set() + + feature_key = (self.feature_name, location) + if feature_key in register[self.feature_version]: + # Don't warn about the same feature multiple times + # FIXME: This is needed to prevent duplicate warnings, but also + # means we won't warn about a feature used in multiple places. + return + register[self.feature_version].add(feature_key) + # Target version is new enough, don't warn even if it is registered for notice + if self.check_version(tv, self.feature_version): + return + self.log_usage_warning(tv, location) + + @classmethod + def report(cls, subproject: str) -> None: + if subproject not in cls.feature_registry: + return + warning_str = cls.get_warning_str_prefix(cls.get_target_version(subproject)) + notice_str = cls.get_notice_str_prefix(cls.get_target_version(subproject)) + fv = cls.feature_registry[subproject] + tv = cls.get_target_version(subproject) + for version in sorted(fv.keys()): + if cls.check_version(tv, version): + notice_str += '\n * {}: {}'.format(version, {i[0] for i in fv[version]}) + else: + warning_str += '\n * {}: {}'.format(version, {i[0] for i in fv[version]}) + if '\n' in notice_str: + mlog.notice(notice_str, fatal=False) + if '\n' in warning_str: + mlog.warning(warning_str) + + def log_usage_warning(self, tv: str, location: T.Optional['mparser.BaseNode']) -> None: + raise InterpreterException('log_usage_warning not implemented') + + @staticmethod + def get_warning_str_prefix(tv: str) -> str: + raise InterpreterException('get_warning_str_prefix not implemented') + + @staticmethod + def get_notice_str_prefix(tv: str) -> str: + raise InterpreterException('get_notice_str_prefix not implemented') + + def __call__(self, f: TV_func) -> TV_func: + @wraps(f) + def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + node, _, _, subproject = get_callee_args(wrapped_args) + if subproject is None: + raise AssertionError(f'{wrapped_args!r}') + self.use(subproject, node) + return f(*wrapped_args, **wrapped_kwargs) + return T.cast('TV_func', wrapped) + + @classmethod + def single_use(cls, feature_name: str, version: str, subproject: 'SubProject', + extra_message: str = '', location: T.Optional['mparser.BaseNode'] = None) -> None: + """Oneline version that instantiates and calls use().""" + cls(feature_name, version, extra_message).use(subproject, location) + + +class FeatureNew(FeatureCheckBase): + """Checks for new features""" + + # Class variable, shared across all instances + # + # Format: {subproject: {feature_version: set(feature_names)}} + feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[T.Tuple[str, T.Optional[mparser.BaseNode]]]]]] + + @staticmethod + def check_version(target_version: str, feature_version: str) -> bool: + return mesonlib.version_compare_condition_with_min(target_version, feature_version) + + @staticmethod + def get_warning_str_prefix(tv: str) -> str: + return f'Project specifies a minimum meson_version \'{tv}\' but uses features which were added in newer versions:' + + @staticmethod + def get_notice_str_prefix(tv: str) -> str: + return '' + + def log_usage_warning(self, tv: str, location: T.Optional['mparser.BaseNode']) -> None: + args = [ + 'Project targets', f"'{tv}'", + 'but uses feature introduced in', + f"'{self.feature_version}':", + f'{self.feature_name}.', + ] + if self.extra_message: + args.append(self.extra_message) + mlog.warning(*args, location=location) + +class FeatureDeprecated(FeatureCheckBase): + """Checks for deprecated features""" + + # Class variable, shared across all instances + # + # Format: {subproject: {feature_version: set(feature_names)}} + feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[T.Tuple[str, T.Optional[mparser.BaseNode]]]]]] + emit_notice = True + + @staticmethod + def check_version(target_version: str, feature_version: str) -> bool: + # For deprecation checks we need to return the inverse of FeatureNew checks + return not mesonlib.version_compare_condition_with_min(target_version, feature_version) + + @staticmethod + def get_warning_str_prefix(tv: str) -> str: + return 'Deprecated features used:' + + @staticmethod + def get_notice_str_prefix(tv: str) -> str: + return 'Future-deprecated features used:' + + def log_usage_warning(self, tv: str, location: T.Optional['mparser.BaseNode']) -> None: + args = [ + 'Project targets', f"'{tv}'", + 'but uses feature deprecated since', + f"'{self.feature_version}':", + f'{self.feature_name}.', + ] + if self.extra_message: + args.append(self.extra_message) + mlog.warning(*args, location=location) + + +# This cannot be a dataclass due to https://github.com/python/mypy/issues/5374 +class FeatureCheckKwargsBase(metaclass=abc.ABCMeta): + + @property + @abc.abstractmethod + def feature_check_class(self) -> T.Type[FeatureCheckBase]: + pass + + def __init__(self, feature_name: str, feature_version: str, + kwargs: T.List[str], extra_message: T.Optional[str] = None): + self.feature_name = feature_name + self.feature_version = feature_version + self.kwargs = kwargs + self.extra_message = extra_message + + def __call__(self, f: TV_func) -> TV_func: + @wraps(f) + def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + node, _, kwargs, subproject = get_callee_args(wrapped_args) + if subproject is None: + raise AssertionError(f'{wrapped_args!r}') + for arg in self.kwargs: + if arg not in kwargs: + continue + name = arg + ' arg in ' + self.feature_name + self.feature_check_class.single_use( + name, self.feature_version, subproject, self.extra_message, node) + return f(*wrapped_args, **wrapped_kwargs) + return T.cast('TV_func', wrapped) + +class FeatureNewKwargs(FeatureCheckKwargsBase): + feature_check_class = FeatureNew + +class FeatureDeprecatedKwargs(FeatureCheckKwargsBase): + feature_check_class = FeatureDeprecated diff --git a/mesonbuild/interpreterbase/disabler.py b/mesonbuild/interpreterbase/disabler.py new file mode 100644 index 0000000..182bb62 --- /dev/null +++ b/mesonbuild/interpreterbase/disabler.py @@ -0,0 +1,45 @@ +# Copyright 2013-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import typing as T + +from .baseobjects import MesonInterpreterObject + +if T.TYPE_CHECKING: + from .baseobjects import TYPE_var, TYPE_kwargs + +class Disabler(MesonInterpreterObject): + def method_call(self, method_name: str, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> TYPE_var: + if method_name == 'found': + return False + return Disabler() + +def _is_arg_disabled(arg: T.Any) -> bool: + if isinstance(arg, Disabler): + return True + if isinstance(arg, list): + for i in arg: + if _is_arg_disabled(i): + return True + return False + +def is_disabled(args: T.Sequence[T.Any], kwargs: T.Dict[str, T.Any]) -> bool: + for i in args: + if _is_arg_disabled(i): + return True + for i in kwargs.values(): + if _is_arg_disabled(i): + return True + return False diff --git a/mesonbuild/interpreterbase/exceptions.py b/mesonbuild/interpreterbase/exceptions.py new file mode 100644 index 0000000..cdbe0fb --- /dev/null +++ b/mesonbuild/interpreterbase/exceptions.py @@ -0,0 +1,33 @@ +# Copyright 2013-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..mesonlib import MesonException + +class InterpreterException(MesonException): + pass + +class InvalidCode(InterpreterException): + pass + +class InvalidArguments(InterpreterException): + pass + +class SubdirDoneRequest(BaseException): + pass + +class ContinueRequest(BaseException): + pass + +class BreakRequest(BaseException): + pass diff --git a/mesonbuild/interpreterbase/helpers.py b/mesonbuild/interpreterbase/helpers.py new file mode 100644 index 0000000..2196b4e --- /dev/null +++ b/mesonbuild/interpreterbase/helpers.py @@ -0,0 +1,56 @@ +# Copyright 2013-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from .. import mesonlib, mparser +from .exceptions import InterpreterException + +import collections.abc +import typing as T + +if T.TYPE_CHECKING: + from .baseobjects import TYPE_var, TYPE_kwargs + +def flatten(args: T.Union['TYPE_var', T.List['TYPE_var']]) -> T.List['TYPE_var']: + if isinstance(args, mparser.StringNode): + assert isinstance(args.value, str) + return [args.value] + if not isinstance(args, collections.abc.Sequence): + return [args] + result: T.List['TYPE_var'] = [] + for a in args: + if isinstance(a, list): + rest = flatten(a) + result = result + rest + elif isinstance(a, mparser.StringNode): + result.append(a.value) + else: + result.append(a) + return result + +def resolve_second_level_holders(args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> T.Tuple[T.List['TYPE_var'], 'TYPE_kwargs']: + def resolver(arg: 'TYPE_var') -> 'TYPE_var': + if isinstance(arg, list): + return [resolver(x) for x in arg] + if isinstance(arg, dict): + return {k: resolver(v) for k, v in arg.items()} + if isinstance(arg, mesonlib.SecondLevelHolder): + return arg.get_default_object() + return arg + return [resolver(x) for x in args], {k: resolver(v) for k, v in kwargs.items()} + +def default_resolve_key(key: mparser.BaseNode) -> str: + if not isinstance(key, mparser.IdNode): + raise InterpreterException('Invalid kwargs format.') + return key.value diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py new file mode 100644 index 0000000..f72ddc1 --- /dev/null +++ b/mesonbuild/interpreterbase/interpreterbase.py @@ -0,0 +1,604 @@ +# Copyright 2016-2017 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This class contains the basic functionality needed to run any interpreter +# or an interpreter-based tool. +from __future__ import annotations + +from .. import mparser, mesonlib +from .. import environment + +from .baseobjects import ( + InterpreterObject, + MesonInterpreterObject, + MutableInterpreterObject, + InterpreterObjectTypeVar, + ObjectHolder, + IterableObject, + + HoldableTypes, +) + +from .exceptions import ( + InterpreterException, + InvalidCode, + InvalidArguments, + SubdirDoneRequest, + ContinueRequest, + BreakRequest +) + +from .decorators import FeatureNew +from .disabler import Disabler, is_disabled +from .helpers import default_resolve_key, flatten, resolve_second_level_holders +from .operator import MesonOperator +from ._unholder import _unholder + +import os, copy, re, pathlib +import typing as T +import textwrap + +if T.TYPE_CHECKING: + from .baseobjects import SubProject, TYPE_kwargs, TYPE_var + from ..interpreter import Interpreter + + HolderMapType = T.Dict[ + T.Union[ + T.Type[mesonlib.HoldableObject], + T.Type[int], + T.Type[bool], + T.Type[str], + T.Type[list], + T.Type[dict], + ], + # For some reason, this has to be a callable and can't just be ObjectHolder[InterpreterObjectTypeVar] + T.Callable[[InterpreterObjectTypeVar, 'Interpreter'], ObjectHolder[InterpreterObjectTypeVar]] + ] + + FunctionType = T.Dict[ + str, + T.Callable[[mparser.BaseNode, T.List[TYPE_var], T.Dict[str, TYPE_var]], TYPE_var] + ] + +class InterpreterBase: + def __init__(self, source_root: str, subdir: str, subproject: 'SubProject'): + self.source_root = source_root + self.funcs: FunctionType = {} + self.builtin: T.Dict[str, InterpreterObject] = {} + # Holder maps store a mapping from an HoldableObject to a class ObjectHolder + self.holder_map: HolderMapType = {} + self.bound_holder_map: HolderMapType = {} + self.subdir = subdir + self.root_subdir = subdir + self.subproject = subproject + self.variables: T.Dict[str, InterpreterObject] = {} + self.argument_depth = 0 + self.current_lineno = -1 + # Current node set during a function call. This can be used as location + # when printing a warning message during a method call. + self.current_node = None # type: mparser.BaseNode + # This is set to `version_string` when this statement is evaluated: + # meson.version().compare_version(version_string) + # If it was part of a if-clause, it is used to temporally override the + # current meson version target within that if-block. + self.tmp_meson_version = None # type: T.Optional[str] + + def load_root_meson_file(self) -> None: + mesonfile = os.path.join(self.source_root, self.subdir, environment.build_filename) + if not os.path.isfile(mesonfile): + raise InvalidArguments(f'Missing Meson file in {mesonfile}') + with open(mesonfile, encoding='utf-8') as mf: + code = mf.read() + if code.isspace(): + raise InvalidCode('Builder file is empty.') + assert isinstance(code, str) + try: + self.ast = mparser.Parser(code, mesonfile).parse() + except mesonlib.MesonException as me: + me.file = mesonfile + raise me + + def parse_project(self) -> None: + """ + Parses project() and initializes languages, compilers etc. Do this + early because we need this before we parse the rest of the AST. + """ + self.evaluate_codeblock(self.ast, end=1) + + def sanity_check_ast(self) -> None: + if not isinstance(self.ast, mparser.CodeBlockNode): + raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.') + if not self.ast.lines: + raise InvalidCode('No statements in code.') + first = self.ast.lines[0] + if not isinstance(first, mparser.FunctionNode) or first.func_name != 'project': + p = pathlib.Path(self.source_root).resolve() + found = p + for parent in p.parents: + if (parent / 'meson.build').is_file(): + with open(parent / 'meson.build', encoding='utf-8') as f: + if f.readline().startswith('project('): + found = parent + break + else: + break + + error = 'first statement must be a call to project()' + if found != p: + raise InvalidCode(f'Not the project root: {error}\n\nDid you mean to run meson from the directory: "{found}"?') + else: + raise InvalidCode(f'Invalid source tree: {error}') + + def run(self) -> None: + # Evaluate everything after the first line, which is project() because + # we already parsed that in self.parse_project() + try: + self.evaluate_codeblock(self.ast, start=1) + except SubdirDoneRequest: + pass + + def evaluate_codeblock(self, node: mparser.CodeBlockNode, start: int = 0, end: T.Optional[int] = None) -> None: + if node is None: + return + if not isinstance(node, mparser.CodeBlockNode): + e = InvalidCode('Tried to execute a non-codeblock. Possibly a bug in the parser.') + e.lineno = node.lineno + e.colno = node.colno + raise e + statements = node.lines[start:end] + i = 0 + while i < len(statements): + cur = statements[i] + try: + self.current_lineno = cur.lineno + self.evaluate_statement(cur) + except Exception as e: + if getattr(e, 'lineno', None) is None: + # We are doing the equivalent to setattr here and mypy does not like it + e.lineno = cur.lineno # type: ignore + e.colno = cur.colno # type: ignore + e.file = os.path.join(self.source_root, self.subdir, environment.build_filename) # type: ignore + raise e + i += 1 # In THE FUTURE jump over blocks and stuff. + + def evaluate_statement(self, cur: mparser.BaseNode) -> T.Optional[InterpreterObject]: + self.current_node = cur + if isinstance(cur, mparser.FunctionNode): + return self.function_call(cur) + elif isinstance(cur, mparser.AssignmentNode): + self.assignment(cur) + elif isinstance(cur, mparser.MethodNode): + return self.method_call(cur) + elif isinstance(cur, mparser.StringNode): + return self._holderify(cur.value) + elif isinstance(cur, mparser.BooleanNode): + return self._holderify(cur.value) + elif isinstance(cur, mparser.IfClauseNode): + return self.evaluate_if(cur) + elif isinstance(cur, mparser.IdNode): + return self.get_variable(cur.value) + elif isinstance(cur, mparser.ComparisonNode): + return self.evaluate_comparison(cur) + elif isinstance(cur, mparser.ArrayNode): + return self.evaluate_arraystatement(cur) + elif isinstance(cur, mparser.DictNode): + return self.evaluate_dictstatement(cur) + elif isinstance(cur, mparser.NumberNode): + return self._holderify(cur.value) + elif isinstance(cur, mparser.AndNode): + return self.evaluate_andstatement(cur) + elif isinstance(cur, mparser.OrNode): + return self.evaluate_orstatement(cur) + elif isinstance(cur, mparser.NotNode): + return self.evaluate_notstatement(cur) + elif isinstance(cur, mparser.UMinusNode): + return self.evaluate_uminusstatement(cur) + elif isinstance(cur, mparser.ArithmeticNode): + return self.evaluate_arithmeticstatement(cur) + elif isinstance(cur, mparser.ForeachClauseNode): + self.evaluate_foreach(cur) + elif isinstance(cur, mparser.PlusAssignmentNode): + self.evaluate_plusassign(cur) + elif isinstance(cur, mparser.IndexNode): + return self.evaluate_indexing(cur) + elif isinstance(cur, mparser.TernaryNode): + return self.evaluate_ternary(cur) + elif isinstance(cur, mparser.FormatStringNode): + if isinstance(cur, mparser.MultilineFormatStringNode): + return self.evaluate_multiline_fstring(cur) + else: + return self.evaluate_fstring(cur) + elif isinstance(cur, mparser.ContinueNode): + raise ContinueRequest() + elif isinstance(cur, mparser.BreakNode): + raise BreakRequest() + else: + raise InvalidCode("Unknown statement.") + return None + + def evaluate_arraystatement(self, cur: mparser.ArrayNode) -> InterpreterObject: + (arguments, kwargs) = self.reduce_arguments(cur.args) + if len(kwargs) > 0: + raise InvalidCode('Keyword arguments are invalid in array construction.') + return self._holderify([_unholder(x) for x in arguments]) + + @FeatureNew('dict', '0.47.0') + def evaluate_dictstatement(self, cur: mparser.DictNode) -> InterpreterObject: + def resolve_key(key: mparser.BaseNode) -> str: + if not isinstance(key, mparser.StringNode): + FeatureNew.single_use('Dictionary entry using non literal key', '0.53.0', self.subproject) + str_key = _unholder(self.evaluate_statement(key)) + if not isinstance(str_key, str): + raise InvalidArguments('Key must be a string') + return str_key + arguments, kwargs = self.reduce_arguments(cur.args, key_resolver=resolve_key, duplicate_key_error='Duplicate dictionary key: {}') + assert not arguments + return self._holderify({k: _unholder(v) for k, v in kwargs.items()}) + + def evaluate_notstatement(self, cur: mparser.NotNode) -> InterpreterObject: + v = self.evaluate_statement(cur.value) + if isinstance(v, Disabler): + return v + return self._holderify(v.operator_call(MesonOperator.NOT, None)) + + def evaluate_if(self, node: mparser.IfClauseNode) -> T.Optional[Disabler]: + assert isinstance(node, mparser.IfClauseNode) + for i in node.ifs: + # Reset self.tmp_meson_version to know if it gets set during this + # statement evaluation. + self.tmp_meson_version = None + result = self.evaluate_statement(i.condition) + if isinstance(result, Disabler): + return result + if not isinstance(result, InterpreterObject): + raise mesonlib.MesonBugException(f'Argument to not ({result}) is not an InterpreterObject but {type(result).__name__}.') + res = result.operator_call(MesonOperator.BOOL, None) + if not isinstance(res, bool): + raise InvalidCode(f'If clause {result!r} does not evaluate to true or false.') + if res: + prev_meson_version = mesonlib.project_meson_versions[self.subproject] + if self.tmp_meson_version: + mesonlib.project_meson_versions[self.subproject] = self.tmp_meson_version + try: + self.evaluate_codeblock(i.block) + finally: + mesonlib.project_meson_versions[self.subproject] = prev_meson_version + return None + if not isinstance(node.elseblock, mparser.EmptyNode): + self.evaluate_codeblock(node.elseblock) + return None + + def evaluate_comparison(self, node: mparser.ComparisonNode) -> InterpreterObject: + val1 = self.evaluate_statement(node.left) + if isinstance(val1, Disabler): + return val1 + val2 = self.evaluate_statement(node.right) + if isinstance(val2, Disabler): + return val2 + + # New code based on InterpreterObjects + operator = { + 'in': MesonOperator.IN, + 'notin': MesonOperator.NOT_IN, + '==': MesonOperator.EQUALS, + '!=': MesonOperator.NOT_EQUALS, + '>': MesonOperator.GREATER, + '<': MesonOperator.LESS, + '>=': MesonOperator.GREATER_EQUALS, + '<=': MesonOperator.LESS_EQUALS, + }[node.ctype] + + # Check if the arguments should be reversed for simplicity (this essentially converts `in` to `contains`) + if operator in (MesonOperator.IN, MesonOperator.NOT_IN): + val1, val2 = val2, val1 + + val1.current_node = node + return self._holderify(val1.operator_call(operator, _unholder(val2))) + + def evaluate_andstatement(self, cur: mparser.AndNode) -> InterpreterObject: + l = self.evaluate_statement(cur.left) + if isinstance(l, Disabler): + return l + l_bool = l.operator_call(MesonOperator.BOOL, None) + if not l_bool: + return self._holderify(l_bool) + r = self.evaluate_statement(cur.right) + if isinstance(r, Disabler): + return r + return self._holderify(r.operator_call(MesonOperator.BOOL, None)) + + def evaluate_orstatement(self, cur: mparser.OrNode) -> InterpreterObject: + l = self.evaluate_statement(cur.left) + if isinstance(l, Disabler): + return l + l_bool = l.operator_call(MesonOperator.BOOL, None) + if l_bool: + return self._holderify(l_bool) + r = self.evaluate_statement(cur.right) + if isinstance(r, Disabler): + return r + return self._holderify(r.operator_call(MesonOperator.BOOL, None)) + + def evaluate_uminusstatement(self, cur: mparser.UMinusNode) -> InterpreterObject: + v = self.evaluate_statement(cur.value) + if isinstance(v, Disabler): + return v + v.current_node = cur + return self._holderify(v.operator_call(MesonOperator.UMINUS, None)) + + def evaluate_arithmeticstatement(self, cur: mparser.ArithmeticNode) -> InterpreterObject: + l = self.evaluate_statement(cur.left) + if isinstance(l, Disabler): + return l + r = self.evaluate_statement(cur.right) + if isinstance(r, Disabler): + return r + + mapping: T.Dict[str, MesonOperator] = { + 'add': MesonOperator.PLUS, + 'sub': MesonOperator.MINUS, + 'mul': MesonOperator.TIMES, + 'div': MesonOperator.DIV, + 'mod': MesonOperator.MOD, + } + l.current_node = cur + res = l.operator_call(mapping[cur.operation], _unholder(r)) + return self._holderify(res) + + def evaluate_ternary(self, node: mparser.TernaryNode) -> T.Optional[InterpreterObject]: + assert isinstance(node, mparser.TernaryNode) + result = self.evaluate_statement(node.condition) + if isinstance(result, Disabler): + return result + result.current_node = node + result_bool = result.operator_call(MesonOperator.BOOL, None) + if result_bool: + return self.evaluate_statement(node.trueblock) + else: + return self.evaluate_statement(node.falseblock) + + @FeatureNew('multiline format strings', '0.63.0') + def evaluate_multiline_fstring(self, node: mparser.MultilineFormatStringNode) -> InterpreterObject: + return self.evaluate_fstring(node) + + @FeatureNew('format strings', '0.58.0') + def evaluate_fstring(self, node: mparser.FormatStringNode) -> InterpreterObject: + assert isinstance(node, mparser.FormatStringNode) + + def replace(match: T.Match[str]) -> str: + var = str(match.group(1)) + try: + val = _unholder(self.variables[var]) + if not isinstance(val, (str, int, float, bool)): + raise InvalidCode(f'Identifier "{var}" does not name a formattable variable ' + + '(has to be an integer, a string, a floating point number or a boolean).') + + return str(val) + except KeyError: + raise InvalidCode(f'Identifier "{var}" does not name a variable.') + + res = re.sub(r'@([_a-zA-Z][_0-9a-zA-Z]*)@', replace, node.value) + return self._holderify(res) + + def evaluate_foreach(self, node: mparser.ForeachClauseNode) -> None: + assert isinstance(node, mparser.ForeachClauseNode) + items = self.evaluate_statement(node.items) + if not isinstance(items, IterableObject): + raise InvalidArguments('Items of foreach loop do not support iterating') + + tsize = items.iter_tuple_size() + if len(node.varnames) != (tsize or 1): + raise InvalidArguments(f'Foreach expects exactly {tsize or 1} variables for iterating over objects of type {items.display_name()}') + + for i in items.iter_self(): + if tsize is None: + if isinstance(i, tuple): + raise mesonlib.MesonBugException(f'Iteration of {items} returned a tuple even though iter_tuple_size() is None') + self.set_variable(node.varnames[0], self._holderify(i)) + else: + if not isinstance(i, tuple): + raise mesonlib.MesonBugException(f'Iteration of {items} did not return a tuple even though iter_tuple_size() is {tsize}') + if len(i) != tsize: + raise mesonlib.MesonBugException(f'Iteration of {items} did not return a tuple even though iter_tuple_size() is {tsize}') + for j in range(tsize): + self.set_variable(node.varnames[j], self._holderify(i[j])) + try: + self.evaluate_codeblock(node.block) + except ContinueRequest: + continue + except BreakRequest: + break + + def evaluate_plusassign(self, node: mparser.PlusAssignmentNode) -> None: + assert isinstance(node, mparser.PlusAssignmentNode) + varname = node.var_name + addition = self.evaluate_statement(node.value) + + # Remember that all variables are immutable. We must always create a + # full new variable and then assign it. + old_variable = self.get_variable(varname) + old_variable.current_node = node + new_value = self._holderify(old_variable.operator_call(MesonOperator.PLUS, _unholder(addition))) + self.set_variable(varname, new_value) + + def evaluate_indexing(self, node: mparser.IndexNode) -> InterpreterObject: + assert isinstance(node, mparser.IndexNode) + iobject = self.evaluate_statement(node.iobject) + if isinstance(iobject, Disabler): + return iobject + index = _unholder(self.evaluate_statement(node.index)) + + if iobject is None: + raise InterpreterException('Tried to evaluate indexing on None') + iobject.current_node = node + return self._holderify(iobject.operator_call(MesonOperator.INDEX, index)) + + def function_call(self, node: mparser.FunctionNode) -> T.Optional[InterpreterObject]: + func_name = node.func_name + (h_posargs, h_kwargs) = self.reduce_arguments(node.args) + (posargs, kwargs) = self._unholder_args(h_posargs, h_kwargs) + if is_disabled(posargs, kwargs) and func_name not in {'get_variable', 'set_variable', 'unset_variable', 'is_disabler'}: + return Disabler() + if func_name in self.funcs: + func = self.funcs[func_name] + func_args = posargs + if not getattr(func, 'no-args-flattening', False): + func_args = flatten(posargs) + if not getattr(func, 'no-second-level-holder-flattening', False): + func_args, kwargs = resolve_second_level_holders(func_args, kwargs) + res = func(node, func_args, kwargs) + return self._holderify(res) if res is not None else None + else: + self.unknown_function_called(func_name) + return None + + def method_call(self, node: mparser.MethodNode) -> T.Optional[InterpreterObject]: + invokable = node.source_object + obj: T.Optional[InterpreterObject] + if isinstance(invokable, mparser.IdNode): + object_display_name = f'variable "{invokable.value}"' + obj = self.get_variable(invokable.value) + else: + object_display_name = invokable.__class__.__name__ + obj = self.evaluate_statement(invokable) + method_name = node.name + (h_args, h_kwargs) = self.reduce_arguments(node.args) + (args, kwargs) = self._unholder_args(h_args, h_kwargs) + if is_disabled(args, kwargs): + return Disabler() + if not isinstance(obj, InterpreterObject): + raise InvalidArguments(f'{object_display_name} is not callable.') + # TODO: InterpreterBase **really** shouldn't be in charge of checking this + if method_name == 'extract_objects': + if isinstance(obj, ObjectHolder): + self.validate_extraction(obj.held_object) + elif not isinstance(obj, Disabler): + raise InvalidArguments(f'Invalid operation "extract_objects" on {object_display_name} of type {type(obj).__name__}') + obj.current_node = node + res = obj.method_call(method_name, args, kwargs) + return self._holderify(res) if res is not None else None + + def _holderify(self, res: T.Union[TYPE_var, InterpreterObject]) -> InterpreterObject: + if isinstance(res, HoldableTypes): + # Always check for an exact match first. + cls = self.holder_map.get(type(res), None) + if cls is not None: + # Casts to Interpreter are required here since an assertion would + # not work for the `ast` module. + return cls(res, T.cast('Interpreter', self)) + # Try the boundary types next. + for typ, cls in self.bound_holder_map.items(): + if isinstance(res, typ): + return cls(res, T.cast('Interpreter', self)) + raise mesonlib.MesonBugException(f'Object {res} of type {type(res).__name__} is neither in self.holder_map nor self.bound_holder_map.') + elif isinstance(res, ObjectHolder): + raise mesonlib.MesonBugException(f'Returned object {res} of type {type(res).__name__} is an object holder.') + elif isinstance(res, MesonInterpreterObject): + return res + raise mesonlib.MesonBugException(f'Unknown returned object {res} of type {type(res).__name__} in the parameters.') + + def _unholder_args(self, + args: T.List[InterpreterObject], + kwargs: T.Dict[str, InterpreterObject]) -> T.Tuple[T.List[TYPE_var], TYPE_kwargs]: + return [_unholder(x) for x in args], {k: _unholder(v) for k, v in kwargs.items()} + + def unknown_function_called(self, func_name: str) -> None: + raise InvalidCode(f'Unknown function "{func_name}".') + + def reduce_arguments( + self, + args: mparser.ArgumentNode, + key_resolver: T.Callable[[mparser.BaseNode], str] = default_resolve_key, + duplicate_key_error: T.Optional[str] = None, + ) -> T.Tuple[ + T.List[InterpreterObject], + T.Dict[str, InterpreterObject] + ]: + assert isinstance(args, mparser.ArgumentNode) + if args.incorrect_order(): + raise InvalidArguments('All keyword arguments must be after positional arguments.') + self.argument_depth += 1 + reduced_pos = [self.evaluate_statement(arg) for arg in args.arguments] + if any(x is None for x in reduced_pos): + raise InvalidArguments('At least one value in the arguments is void.') + reduced_kw: T.Dict[str, InterpreterObject] = {} + for key, val in args.kwargs.items(): + reduced_key = key_resolver(key) + assert isinstance(val, mparser.BaseNode) + reduced_val = self.evaluate_statement(val) + if reduced_val is None: + raise InvalidArguments(f'Value of key {reduced_key} is void.') + if duplicate_key_error and reduced_key in reduced_kw: + raise InvalidArguments(duplicate_key_error.format(reduced_key)) + reduced_kw[reduced_key] = reduced_val + self.argument_depth -= 1 + final_kw = self.expand_default_kwargs(reduced_kw) + return reduced_pos, final_kw + + def expand_default_kwargs(self, kwargs: T.Dict[str, T.Optional[InterpreterObject]]) -> T.Dict[str, T.Optional[InterpreterObject]]: + if 'kwargs' not in kwargs: + return kwargs + to_expand = _unholder(kwargs.pop('kwargs')) + if not isinstance(to_expand, dict): + raise InterpreterException('Value of "kwargs" must be dictionary.') + if 'kwargs' in to_expand: + raise InterpreterException('Kwargs argument must not contain a "kwargs" entry. Points for thinking meta, though. :P') + for k, v in to_expand.items(): + if k in kwargs: + raise InterpreterException(f'Entry "{k}" defined both as a keyword argument and in a "kwarg" entry.') + kwargs[k] = self._holderify(v) + return kwargs + + def assignment(self, node: mparser.AssignmentNode) -> None: + assert isinstance(node, mparser.AssignmentNode) + if self.argument_depth != 0: + raise InvalidArguments(textwrap.dedent('''\ + Tried to assign values inside an argument list. + To specify a keyword argument, use : instead of =. + ''')) + var_name = node.var_name + if not isinstance(var_name, str): + raise InvalidArguments('Tried to assign value to a non-variable.') + value = self.evaluate_statement(node.value) + # For mutable objects we need to make a copy on assignment + if isinstance(value, MutableInterpreterObject): + value = copy.deepcopy(value) + self.set_variable(var_name, value) + + def set_variable(self, varname: str, variable: T.Union[TYPE_var, InterpreterObject], *, holderify: bool = False) -> None: + if variable is None: + raise InvalidCode('Can not assign None to variable.') + if holderify: + variable = self._holderify(variable) + else: + # Ensure that we are always storing ObjectHolders + if not isinstance(variable, InterpreterObject): + raise mesonlib.MesonBugException(f'set_variable in InterpreterBase called with a non InterpreterObject {variable} of type {type(variable).__name__}') + if not isinstance(varname, str): + raise InvalidCode('First argument to set_variable must be a string.') + if re.match('[_a-zA-Z][_0-9a-zA-Z]*$', varname) is None: + raise InvalidCode('Invalid variable name: ' + varname) + if varname in self.builtin: + raise InvalidCode(f'Tried to overwrite internal variable "{varname}"') + self.variables[varname] = variable + + def get_variable(self, varname: str) -> InterpreterObject: + if varname in self.builtin: + return self.builtin[varname] + if varname in self.variables: + return self.variables[varname] + raise InvalidCode(f'Unknown variable "{varname}".') + + def validate_extraction(self, buildtarget: mesonlib.HoldableObject) -> None: + raise InterpreterException('validate_extraction is not implemented in this context (please file a bug)') diff --git a/mesonbuild/interpreterbase/operator.py b/mesonbuild/interpreterbase/operator.py new file mode 100644 index 0000000..5dec8d0 --- /dev/null +++ b/mesonbuild/interpreterbase/operator.py @@ -0,0 +1,32 @@ +# SPDX-license-identifier: Apache-2.0 + +from enum import Enum + +class MesonOperator(Enum): + # Arithmetic + PLUS = '+' + MINUS = '-' + TIMES = '*' + DIV = '/' + MOD = '%' + + UMINUS = 'uminus' + + # Logic + NOT = 'not' + + # Should return the boolsche interpretation of the value (`'' == false` for instance) + BOOL = 'bool()' + + # Comparison + EQUALS = '==' + NOT_EQUALS = '!=' + GREATER = '>' + LESS = '<' + GREATER_EQUALS = '>=' + LESS_EQUALS = '<=' + + # Container + IN = 'in' + NOT_IN = 'not in' + INDEX = '[]' |