Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
471facd
interpreter: Replace last uses of Feature*Kwargs
dcbaker Jul 13, 2026
57b44b9
interpreterbase/decorators: pull some helpers out of typed_kwargs
dcbaker Jul 21, 2026
85201f6
interpreterbase/decorators: share code between pos and kwarg checking
dcbaker Jul 21, 2026
7e4308f
decorators: share error formatting code between pos and kw arg checking
dcbaker Jul 22, 2026
407cd7a
decorators: normalize quoting of types in errors
dcbaker Jul 22, 2026
4b3dd8b
decorators: make the quoting of types fully standard
dcbaker Jul 22, 2026
263c844
decorators: consistently quote the name of functions in errors
dcbaker Jul 22, 2026
0a9ca80
decorators: Convert KwargInfo to a dataclass
dcbaker Jul 22, 2026
252fd63
decorators: use some `TypeAlias`es to make further changes easier
dcbaker Jul 22, 2026
6ad1137
decorators: allow tuples of types in KWargInfo since_values and depre…
dcbaker Jul 22, 2026
ed0ad23
modules/cmake: Use KwargInfo since_values instead of open coding
dcbaker Jul 22, 2026
a4c81a7
modules/qt: Use KWargInfo.since_values
dcbaker Jul 22, 2026
86248c5
modules/qt: Use typed_pos_args for qt.preprocess
dcbaker Jul 22, 2026
0ded72e
modules/sourceset: Use KwargInfo.since_values for new types
dcbaker Jul 22, 2026
08187b3
interpreterobjects: move pkgconfig_define FeatureNew to KWargInfo
dcbaker Jul 22, 2026
6f71961
interpreter: Use KWargInfo for some test arguments
dcbaker Jul 22, 2026
d6ea643
interpreter/type_checking: consistently quote in_set_validator errors
dcbaker Jul 23, 2026
6821d80
interpreter/type_checking: quote unknown langauge validator error
dcbaker Jul 23, 2026
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
12 changes: 9 additions & 3 deletions mesonbuild/interpreter/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..compilers import SUFFIX_TO_LANG, RunResult
from ..compilers.compilers import CompileCheckMode
from ..interpreterbase import (ObjectHolder, noPosargs, noKwargs,
FeatureNew, FeatureNewKwargs, disablerIfNotFound,
FeatureNew, disablerIfNotFound,
InterpreterException, InterpreterObject)
from ..interpreterbase.decorators import ContainerTypeInfo, typed_kwargs, KwargInfo, typed_pos_args
from ..options import OptionKey
Expand Down Expand Up @@ -881,12 +881,18 @@ def get_argument_syntax_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwa
return self.compiler.get_argument_syntax()

@FeatureNew('compiler.preprocess', '0.64.0')
@FeatureNewKwargs('compiler.preprocess', '1.3.2', ['compile_args'], extra_message='compile_args were ignored before this version')
@typed_pos_args('compiler.preprocess', varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList), min_varargs=1)
@typed_kwargs(
'compiler.preprocess',
KwargInfo('output', str, default='@PLAINNAME@.i'),
KwargInfo('compile_args', ContainerTypeInfo(list, str), listify=True, default=[]),
KwargInfo(
'compile_args',
ContainerTypeInfo(list, str),
listify=True,
default=[],
since='1.3.2',
since_message='compile_args were ignored before this version',
),
_INCLUDE_DIRECTORIES_KW,
_DEPENDENCIES_KW.evolve(since='1.1.0'),
_DEPENDS_KW.evolve(since='1.4.0'),
Expand Down
3 changes: 1 addition & 2 deletions mesonbuild/interpreter/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from .decorators import apply_machine_map
from ..interpreterbase import InterpreterException, InvalidArguments, InvalidCode, SubdirDoneRequest
from ..interpreterbase import Disabler, disablerIfNotFound
from ..interpreterbase import FeatureNew, FeatureDeprecated, FeatureBroken, FeatureNewKwargs
from ..interpreterbase import FeatureNew, FeatureDeprecated, FeatureBroken
from ..interpreterbase import ObjectHolder, ContextManagerObject, DefaultObject
from ..interpreterbase import stringifyUserArguments, Feature, FeatureValue
from ..modules import ExtensionModule, ModuleObject, MutableModuleObject, NewExtensionModule, NotFoundExtensionModule, __path__ as modules_path
Expand Down Expand Up @@ -2037,7 +2037,6 @@ def func_jar(self, node: mparser.BaseNode,
kwargs: kwtypes.Jar) -> build.Jar:
return self.build_target(node, T.cast('tuple[str, SourcesVarargsType]', args), kwargs, build.Jar)

@FeatureNewKwargs('build_target', '0.40.0', ['link_whole', 'override_options'])
@typed_pos_args('build_target', str, varargs=SOURCES_VARARGS)
@typed_kwargs('build_target', *BUILD_TARGET_KWS)
def func_build_target(self, node: mparser.BaseNode,
Expand Down
3 changes: 2 additions & 1 deletion mesonbuild/interpreter/type_checking.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ def _bt_install_dir_deprecated(args: T.List[T.Union[str, bool]]) -> T.Iterator[F

# Applies to all build_target like classes
_ALL_TARGET_KWS: T.List[KwargInfo] = [
OVERRIDE_OPTIONS_KW,
OVERRIDE_OPTIONS_KW.evolve(since='0.40.0'),
KwargInfo('build_by_default', bool, default=True, since='0.38.0'),
DEPENDENCIES_KW,
KwargInfo(
Expand Down Expand Up @@ -784,6 +784,7 @@ def _pch_convertor(args: T.List[str]) -> T.Optional[T.Tuple[str, T.Optional[str]
INCLUDE_DIRECTORIES.evolve(name='d_import_dirs'),
LINK_ARGS_KW,
LINK_WHOLE_KW.evolve(
since='0.40.0',
as_default=[('', ('1.11.0', "Replace an empty string with an empty array: `link_whole : ''` -> `link_whole : []`"))],
),
_NAME_PREFIX_KW,
Expand Down
4 changes: 0 additions & 4 deletions mesonbuild/interpreterbase/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@
'FeatureNew',
'FeatureDeprecated',
'FeatureBroken',
'FeatureNewKwargs',
'FeatureDeprecatedKwargs',

'InterpreterBase',

Expand Down Expand Up @@ -104,8 +102,6 @@
FeatureNew,
FeatureDeprecated,
FeatureBroken,
FeatureNewKwargs,
FeatureDeprecatedKwargs,
)

from .exceptions import (
Expand Down
123 changes: 43 additions & 80 deletions mesonbuild/interpreterbase/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,44 @@ def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var:
return inner


def _types_description(types: tuple[type | ContainerTypeInfo, ...] | type | ContainerTypeInfo) -> str:
candidates: list[str] = []
types_tuple = types if isinstance(types, tuple) else (types, )
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[type | ContainerTypeInfo, ...] | type | ContainerTypeInfo,
value: T.Any) -> bool:
types_tuple = types if isinstance(types, tuple) else (types, )
for t in types_tuple:
if isinstance(t, ContainerTypeInfo):
if t.check(value):
return True
elif isinstance(value, t):
return True
return False


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,
Expand Down Expand Up @@ -251,12 +289,7 @@ def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
else:
msg = 'not allowed for required positional arguments'
raise InvalidArguments(f'default() objects are {msg}')

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}')
raise InvalidArguments(f'{name} argument {i} was of type "{_raw_description(arg)}" but should have been {_types_description(type_)}')

@dnicolodi dnicolodi Jul 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Any reason to remove the double quotes around {_types_description(type_)}? I find having the quotes around the actual type but not around the expected type a distracting asymmetry.


# Ensure that we're actually passing a tuple.
# Depending on what kind of function we're calling the length of
Expand Down Expand Up @@ -504,39 +537,6 @@ def typed_kwargs(name: str, *types: KwargInfo, allow_unknown: bool = False) -> T
"""
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:

Expand Down Expand Up @@ -606,7 +606,7 @@ def emit_feature_change(values: T.Dict[_T, T.Union[str, T.Tuple[str, str]]], fea
value = copy.copy(info.default)
if info.listify:
kwargs[info.name] = value = mesonlib.listify(value)
if not check_value_type(types_tuple, value):
if not _check_value_type(types_tuple, value):
extra_desc: T.List[str] = []
if info.extra_types:
if isinstance(value, list):
Expand All @@ -618,10 +618,10 @@ def emit_feature_change(values: T.Dict[_T, T.Union[str, T.Tuple[str, str]]], fea
if isinstance(value, t):
extra_desc.append(cb(value))

shouldbe = types_description(types_tuple)
shouldbe = _types_description(types_tuple)
if extra_desc:
shouldbe = '{}. {}'.format(shouldbe, '. '.join(extra_desc))
raise InvalidArguments(f'{name} keyword argument {info.name!r} was of type {raw_description(value)} but should have been {shouldbe}')
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)
Expand All @@ -643,7 +643,7 @@ def emit_feature_change(values: T.Dict[_T, T.Union[str, T.Tuple[str, str]]], fea
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 function {name} default value of {info.name} is not a valid type, got {type(info.default)} expected {types_description(types_tuple)}'
assert _check_value_type(types_tuple, info.default), f'In function {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)
Expand Down Expand Up @@ -883,40 +883,3 @@ def log_usage_warning(self, tv: MesonVersionTarget, location: T.Optional['mparse
if self.extra_message:
args.append(self.extra_message)
mlog.deprecation(*args, location=location)


# This cannot be a dataclass due to https://github.com/python/mypy/issues/5374
class FeatureCheckKwargsBase(metaclass=mesonlib.SimpleABC):

@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
6 changes: 3 additions & 3 deletions unittests/internaltests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1192,7 +1192,7 @@ def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:

with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 1.0, False], None)
self.assertEqual(str(cm.exception), 'foo argument 2 was of type "float" but should have been "int"')
self.assertEqual(str(cm.exception), 'foo argument 2 was of type "float" but should have been int')

def test_typed_pos_args_types_wrong_number(self) -> None:
@typed_pos_args('foo', str, int, bool)
Expand Down Expand Up @@ -1235,7 +1235,7 @@ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:

with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 'var', 'args', 0], None)
self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been "str"')
self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been str')

def test_typed_pos_args_varargs_invalid_multiple_types(self) -> None:
@typed_pos_args('foo', str, varargs=(str, list))
Expand All @@ -1244,7 +1244,7 @@ def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:

with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 'var', 'args', 0], None)
self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been one of: "str", "list"')
self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been one of: str, list')

def test_typed_pos_args_max_varargs(self) -> None:
@typed_pos_args('foo', str, varargs=str, max_varargs=5)
Expand Down