interpreterbase: Add support for positional arguments to TypedArgs

This commit is contained in:
Dylan Baker authored and Paolo Bonzini committed 2026-09-21 10:31:12 +02:00
1 parent d8d074ad60
commit e31a5b78f7
5 files changed
+692 -54

No files matched your search

+24 -1
View File
@@ -14,7 +14,10 @@ from ..build import (CustomTarget, BuildTarget,
from ..options import OptionKey
from ..dependencies import Dependency, DependencyMethods, InternalDependency
from ..interpreterbase import Feature
from ..interpreterbase.decorators import KwargInfo, ContainerTypeInfo, FeatureBroken, FeatureDeprecated, FeatureNew
from ..interpreterbase.decorators import (
KwargInfo, ContainerTypeInfo, FeatureBroken, FeatureDeprecated, FeatureNew,
PosArgInfo, OptArgInfo, VarArgInfo,
)
from ..mesonlib import (File, FileMode, MachineChoice, has_path_sep, listify, stringlistify,
EnvironmentVariables)
from ..programs import Program, ExternalProgram
@@ -1157,3 +1160,23 @@ DEPENDENCY_KWS: T.List[KwargInfo] = [
KwargInfo('static', (bool, NoneType)),
KwargInfo('version', ContainerTypeInfo(list, str), listify=True, default=[]),
]
STR_PARG = PosArgInfo(str)
STR_FILE_PARG = PosArgInfo((str, File))
INT_PARG = PosArgInfo(int)
BOOL_PARG = PosArgInfo(bool)
OBJ_PARG = PosArgInfo(object)
STR_OARG = OptArgInfo(str)
INT_OARG = OptArgInfo(int)
BOOL_OARG = OptArgInfo(bool)
OBJ_OARG = OptArgInfo(object)
STR_VARG = VarArgInfo(str)
STR_VARG_1 = VarArgInfo(str, min_args=1)
STR_FILE_VARG = VarArgInfo((str, File))
INT_VARG = VarArgInfo(int)
BOOL_VARG = VarArgInfo(bool)
OBJ_VARG = VarArgInfo(object)
SRC_VARG = VarArgInfo(SOURCES_VARARGS)
TGT_VARG = VarArgInfo((str, File, CustomTarget, CustomTargetIndex, GeneratedList))
+6 -1
View File
@@ -39,11 +39,13 @@ __all__ = [
'ContainerTypeInfo',
'KwargInfo',
'TypedArgs',
'PosArgInfo',
'OptArgInfo',
'VarArgInfo',
'FeatureCheckBase',
'FeatureNew',
'FeatureDeprecated',
'FeatureBroken',
'TypedArgs',
'InterpreterBase',
@@ -97,6 +99,9 @@ from .decorators import (
FeatureDeprecated,
FeatureBroken,
TypedArgs,
PosArgInfo,
OptArgInfo,
VarArgInfo,
)
from .exceptions import (
+231 -2
View File
@@ -63,6 +63,32 @@ if T.TYPE_CHECKING:
extra_types: T.Mapping[type, T.Callable[[object], str]] | None
as_default: list[tuple[object, str | tuple[str, str]]] | None
class _PosArgKWs(T.TypedDict, total=False):
since: str | None
since_message: str
since_values: _FeatureValues | None
deprecated: str | None
deprecated_message: str
deprecated_values: _FeatureValues | None
feature_validator: T.Callable[[T.Any], T.Iterable[FeatureCheckBase]] | None
validator: T.Callable[[T.Any], str | None] | None
convertor: T.Callable[[T.Any], object] | None
listify: bool
class _OptArgKWs(_PosArgKWs, total=False):
default: TYPE_var | None
optional_since: str | None
optional_since_message: str
class _VarArgKWs(_PosArgKWs, total=False):
min_args: int
max_args: int
variadic_since: str | None
variadic_since_message: str
def is_module(obj: object) -> TypeIs[ModuleObject]:
return not hasattr(obj, 'current_node')
@@ -492,10 +518,60 @@ class KwargInfo(T.Generic[_T]):
return dataclasses.replace(self, **kwargs)
@dataclasses.dataclass(slots=True, eq=False)
class _PosArgInfoBase:
types: type | T.Tuple[type | ContainerTypeInfo, ...] | ContainerTypeInfo
since: str | None = dataclasses.field(default=None, kw_only=True)
since_message: str = dataclasses.field(default='', kw_only=True)
since_values: _FeatureValues | None = dataclasses.field(default=None, kw_only=True)
deprecated: str | None = dataclasses.field(default=None, kw_only=True)
deprecated_message: str = dataclasses.field(default='', kw_only=True)
deprecated_values: _FeatureValues | None = dataclasses.field(default=None, kw_only=True)
feature_validator: T.Callable[[T.Any], T.Iterable[FeatureCheckBase]] | None = \
dataclasses.field(default=None, kw_only=True)
validator: T.Optional[T.Callable[[T.Any], str | None]] = dataclasses.field(default=None, kw_only=True)
convertor: T.Optional[T.Callable[[T.Any], object]] = dataclasses.field(default=None, kw_only=True)
listify: bool = dataclasses.field(default=False, kw_only=True)
@dataclasses.dataclass(slots=True, eq=False)
class PosArgInfo(_PosArgInfoBase):
def evolve(self, **kwargs: Unpack[_PosArgKWs]) -> PosArgInfo:
return dataclasses.replace(self, **kwargs)
@dataclasses.dataclass(slots=True, eq=False)
class OptArgInfo(_PosArgInfoBase):
default: TYPE_var | None = dataclasses.field(default=None, kw_only=True)
optional_since: str | None = dataclasses.field(default=None, kw_only=True)
optional_since_message: str = dataclasses.field(default='', kw_only=True)
def evolve(self, **kwargs: Unpack[_OptArgKWs]) -> OptArgInfo:
return dataclasses.replace(self, **kwargs)
@dataclasses.dataclass(slots=True, eq=False)
class VarArgInfo(_PosArgInfoBase):
min_args: int = dataclasses.field(default=0, kw_only=True)
max_args: int = dataclasses.field(default=0, kw_only=True)
variadic_since: str | None = dataclasses.field(default=None, kw_only=True)
variadic_since_message: str = dataclasses.field(default='', kw_only=True)
def evolve(self, **kwargs: Unpack[_VarArgKWs]) -> VarArgInfo:
return dataclasses.replace(self, **kwargs)
@dataclasses.dataclass(slots=True, eq=False)
class TypedArgs:
name: str
pos_types: list[PosArgInfo] | None = dataclasses.field(default=None, kw_only=True)
opt_types: list[OptArgInfo] = dataclasses.field(default_factory=list, kw_only=True)
var_types: VarArgInfo | None = dataclasses.field(default=None, kw_only=True)
kw_types: list[KwargInfo] = dataclasses.field(default_factory=list, kw_only=True)
unknown_kwargs: bool = dataclasses.field(default=False, kw_only=True)
@@ -533,7 +609,7 @@ class TypedArgs:
def _process_kwargs(self, node: mparser.BaseNode | None, _kwargs: TYPE_kwargs, subproject: SubProject) -> None:
# 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)
make_err = partial(InvalidArguments.from_node, node=node) if node else InvalidArguments
make_err = partial(InvalidArguments.from_node, node=node) if node is not None else InvalidArguments
if not self.unknown_kwargs:
all_names = {t.name for t in self.kw_types}
@@ -623,13 +699,166 @@ class TypedArgs:
if info.convertor:
kwargs[info.name] = info.convertor(kwargs[info.name])
def _pw_emit_feature_change(self, value: object, values: T.Dict[_T, T.Union[str, T.Tuple[str, str]]],
feature: T.Type[FeatureDeprecated | FeatureNew],
subproject: SubProject, node: mparser.BaseNode | None,
index: int) -> None:
for n, version in values.items():
if isinstance(version, tuple):
version, msg = version
else:
msg = ''
warning: T.Optional[str] = None
if isinstance(n, ContainerTypeInfo):
if n.check_any(value):
d, extra = n.description()
warning = f'of type "{d}"'
if extra:
warning = f'{warning} {extra}'
elif isinstance(n, (type, tuple)):
if isinstance(value, n):
warning = f'of type "{type(value).__name__}"'
elif isinstance(value, list):
if n in value:
warning = f'value "{n}" in list'
elif isinstance(value, dict):
if n in value:
warning = f'value "{n}" in dict keys'
elif n == value:
warning = f'value "{n}"'
if warning:
feature.single_use(f'"{self.name}" positional argument "{index}" {warning}', version, subproject, msg, location=node)
def _process_args(self, node: mparser.BaseNode | None, args: list[TYPE_var], subproject: SubProject) -> tuple[object, ...] | None:
if self.pos_types is None and not (self.opt_types or self.var_types):
return None
assert not (self.opt_types and self.var_types), \
'Cannot use optional arguments and variadic arguments together due to ambiguity'
nargs = T.cast('list[object]', args).copy()
num_args = len(args)
num_types = len(self.pos_types or [])
types: tuple[_PosArgInfoBase, ...] = tuple(self.pos_types or [])
make_err = partial(InvalidArguments.from_node, node=node) if node is not None else InvalidArguments
if self.var_types:
min_args = num_types + self.var_types.min_args
max_args = num_types + self.var_types.max_args
if self.var_types.max_args == 0 and num_args < min_args:
raise make_err(f'"{self.name}" takes at least {min_args} arguments, but got {num_args}.')
elif self.var_types.max_args != 0 and (num_args < min_args or num_args > max_args):
raise make_err(f'"{self.name}" takes between {min_args} and {max_args} arguments, but got {num_args}.')
elif self.opt_types:
if num_args < num_types:
raise make_err(f'"{self.name}" takes at least {num_types} arguments, but got {num_args}.')
num_types_tot = num_types + len(self.opt_types)
if num_args > num_types_tot:
raise make_err(f'"{self.name}" takes at most {num_types_tot} arguments, but got {num_args}.')
# The number of unset optional arguments we need to use the default values for
unset_opt_args = num_args - num_types
# Add the types to cover optional arguments that have been passed
types = tuple(list(types) + list(self.opt_types[:unset_opt_args]))
assert len(types) == num_args, (len(types), num_args)
elif num_args != num_types:
raise make_err(f'"{self.name}" takes exactly {num_types} arguments, but got {num_args}.')
for i, (value, info) in enumerate(itertools.zip_longest(args, types, fillvalue=self.var_types), start=1):
assert info is not None, 'We should never get a None info'
types_tuple = info.types if isinstance(info.types, tuple) else (info.types,)
if isinstance(value, DefaultObject) and DefaultObject not in types_tuple:
if isinstance(info, PosArgInfo):
raise make_err('default() objects are not allowed for required positional arguments')
elif isinstance(info, OptArgInfo):
FeatureNew.single_use('default() object for optional positional arguments', '1.13.0',
subproject, location=node)
# Replace the DefaultObject with the default value, but skip further validation.
# If the default is None, and we don't have None in the types_tuple (common),
# then we'll fail validation later.
nargs[i - 1] = value = copy.copy(info.default)
continue
else:
raise make_err('default() objects are not allowed for variadic arguments')
if info.since:
feature_name = f'positional argument "{i}" in {self.name}'
FeatureNew.single_use(feature_name, info.since, subproject, info.since_message, location=node)
if info.deprecated:
feature_name = f'positional argument "{i}" in {self.name}'
FeatureDeprecated.single_use(feature_name, info.deprecated, subproject, info.deprecated_message, location=node)
if info.listify:
nargs[i - 1] = value = mesonlib.listify(value)
if not _check_value_type(types_tuple, value):
raise make_err(_shouldbe_format(self.name, 'positional', str(i), value, types_tuple))
if info.validator is not None:
msg = info.validator(value)
if msg is not None:
raise make_err(f'"{self.name}" positional argument "{i}" {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:
self._pw_emit_feature_change(
value, info.deprecated_values, FeatureDeprecated, subproject, node, i)
if info.since_values is not None:
self._pw_emit_feature_change(
value, info.since_values, FeatureNew, subproject, node, i)
if info.convertor:
nargs[i - 1] = info.convertor(value)
if self.opt_types:
for i, info in enumerate(self.opt_types[unset_opt_args:], start=num_args):
if info.optional_since:
feature_name = f'positional argument "{i}" in {self.name} as optional'
FeatureNew.single_use(feature_name, info.optional_since, subproject,
info.optional_since_message, node)
nargs.append(copy.copy(info.default))
elif self.var_types:
# 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 = nargs[:len(types)]
var = list(nargs[len(types):])
nargs = pos
nargs.append(var)
if self.var_types.variadic_since is not None and len(var) > 1:
FeatureNew.single_use(f'"{self.name}": More than one variadic argument',
self.var_types.variadic_since,
subproject, location=node)
return tuple(nargs)
# TODO: need to use two different types here to avoid passing the original type through
def __call__(self, f: TV_func) -> T.Callable[..., T.Any]:
@wraps(f)
def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
node, _, _kwargs, subproject = get_callee_args(wrapped_args)
node, _args, _kwargs, subproject = get_callee_args(wrapped_args)
assert _args is not None, 'for mypy'
assert _kwargs is not None, 'for mypy'
self._process_kwargs(node, _kwargs, subproject)
args = self._process_args(node, _args, subproject)
if args is not None:
w = list(wrapped_args)
i = w.index(_args)
w[i] = args
wrapped_args = tuple(w)
return f(*wrapped_args, **wrapped_kwargs)
return T.cast('T.Callable[..., T.Any]', wrapper)
+2 -4
View File
@@ -34,10 +34,6 @@ testcase expect_error('default() objects are not allowed for variadic arguments'
add_languages(default())
endtestcase
testcase expect_error('default() objects are not allowed for optional positional arguments')
assert(true, default())
endtestcase
testcase expect_error('"install_symlink" got a default() value for the required keyword argument "install_dir". default() may not be used for required keyword arguments.')
install_symlink('foo', pointing_to : '/foo', install_dir : default())
endtestcase
@@ -46,3 +42,5 @@ foo = default()
get_variable('foo')
unset_variable('foo')
set_variable('var', default())
assert(true, default())
+429 -46
View File
@@ -37,15 +37,20 @@ from mesonbuild.compilers.d import DmdDCompiler, LLVMDCompiler
from mesonbuild.compilers.detect import detect_c_compiler
from mesonbuild.compilers.mixins.visualstudio import MSVCCompiler, ClangClCompiler
from mesonbuild.linkers import linkers
from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, ObjectHolder
from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, TypedArgs, ContainerTypeInfo, KwargInfo
from mesonbuild.interpreterbase import (
ObjectHolder, DefaultObject, InvalidArguments, TypedArgs, ContainerTypeInfo,
KwargInfo, PosArgInfo, VarArgInfo,
)
from mesonbuild.mesonlib import (
LibType, MachineChoice, PerMachine, SimpleABC, Version, is_windows, is_osx,
is_cygwin, is_openbsd, search_version, MesonException, EnvironmentException, python_command,
version_check_to_range,
)
from mesonbuild.options import OptionKey
from mesonbuild.interpreter.type_checking import in_set_validator, NoneType
from mesonbuild.interpreter.type_checking import (
STR_PARG, INT_PARG, BOOL_PARG, STR_OARG, INT_OARG, STR_VARG,
in_set_validator, NoneType,
)
from mesonbuild.dependencies.pkgconfig import PkgConfigDependency, PkgConfigInterface, PkgConfigCLI
from mesonbuild.programs import ExternalProgram
import mesonbuild.modules.pkgconfig
@@ -1509,39 +1514,43 @@ Thread model: posix'''), '21.9.0')
self.assertFalse(errors)
def test_typed_pos_args_types(self) -> None:
@typed_pos_args('foo', str, int, bool)
@TypedArgs('foo', pos_types=[STR_PARG, INT_PARG, BOOL_PARG])
def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:
self.assertIsInstance(args, tuple)
self.assertIsInstance(args[0], str)
self.assertIsInstance(args[1], int)
self.assertIsInstance(args[2], bool)
_(None, mock.Mock(), ['string', 1, False], None)
_(None, mock.Mock(), ['string', 1, False], {})
def test_typed_pos_args_types_invalid(self) -> None:
@typed_pos_args('foo', str, int, bool)
@TypedArgs('foo', pos_types=[STR_PARG, INT_PARG, BOOL_PARG])
def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 1.0, False], None)
_(None, mock.Mock(), ['string', 1.0, False], {})
self.assertEqual(str(cm.exception), '"foo" positional 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)
@TypedArgs('foo', pos_types=[STR_PARG, INT_PARG, BOOL_PARG])
def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 1], None)
_(None, mock.Mock(), ['string', 1], {})
self.assertEqual(str(cm.exception), '"foo" takes exactly 3 arguments, but got 2.')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 1, True, True], None)
_(None, mock.Mock(), ['string', 1, True, True], {})
self.assertEqual(str(cm.exception), '"foo" takes exactly 3 arguments, but got 4.')
def test_typed_pos_args_varargs(self) -> None:
@typed_pos_args('foo', str, varargs=str)
@TypedArgs(
'foo',
pos_types=[STR_PARG],
var_types=STR_VARG,
)
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
self.assertIsInstance(args, tuple)
self.assertIsInstance(args[0], str)
@@ -1549,38 +1558,54 @@ Thread model: posix'''), '21.9.0')
self.assertIsInstance(args[1][0], str)
self.assertIsInstance(args[1][1], str)
_(None, mock.Mock(), ['string', 'var', 'args'], None)
_(None, mock.Mock(), ['string', 'var', 'args'], {})
def test_typed_pos_args_varargs_not_given(self) -> None:
@typed_pos_args('foo', str, varargs=str)
@TypedArgs(
'foo',
pos_types=[STR_PARG],
var_types=STR_VARG,
)
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
self.assertIsInstance(args, tuple)
self.assertIsInstance(args[0], str)
self.assertIsInstance(args[1], list)
self.assertEqual(args[1], [])
_(None, mock.Mock(), ['string'], None)
_(None, mock.Mock(), ['string'], {})
def test_typed_pos_args_varargs_invalid(self) -> None:
@typed_pos_args('foo', str, varargs=str)
@TypedArgs(
'foo',
pos_types=[STR_PARG],
var_types=STR_VARG,
)
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 'var', 'args', 0], None)
_(None, mock.Mock(), ['string', 'var', 'args', 0], {})
self.assertEqual(str(cm.exception), '"foo" positional 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))
@TypedArgs(
'foo',
pos_types=[STR_PARG],
var_types=VarArgInfo((str, list)),
)
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 'var', 'args', 0], None)
_(None, mock.Mock(), ['string', 'var', 'args', 0], {})
self.assertEqual(str(cm.exception), '"foo" positional 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)
@TypedArgs(
'foo',
pos_types=[STR_PARG],
var_types=STR_VARG.evolve(max_args=5),
)
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
self.assertIsInstance(args, tuple)
self.assertIsInstance(args[0], str)
@@ -1588,95 +1613,135 @@ Thread model: posix'''), '21.9.0')
self.assertIsInstance(args[1][0], str)
self.assertIsInstance(args[1][1], str)
_(None, mock.Mock(), ['string', 'var', 'args'], None)
_(None, mock.Mock(), ['string', 'var', 'args'], {})
def test_typed_pos_args_max_varargs_exceeded(self) -> None:
@typed_pos_args('foo', str, varargs=str, max_varargs=1)
@TypedArgs(
'foo',
pos_types=[STR_PARG],
var_types=STR_VARG.evolve(max_args=1),
)
def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 'var', 'args'], None)
_(None, mock.Mock(), ['string', 'var', 'args'], {})
self.assertEqual(str(cm.exception), '"foo" takes between 1 and 2 arguments, but got 3.')
def test_typed_pos_args_min_varargs(self) -> None:
@typed_pos_args('foo', varargs=str, max_varargs=2, min_varargs=1)
@TypedArgs(
'foo',
var_types=STR_VARG.evolve(max_args=2, min_args=1),
)
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
self.assertIsInstance(args, tuple)
self.assertIsInstance(args[0], list)
self.assertIsInstance(args[0][0], str)
self.assertIsInstance(args[0][1], str)
_(None, mock.Mock(), ['string', 'var'], None)
_(None, mock.Mock(), ['string', 'var'], {})
def test_typed_pos_args_min_varargs_not_met(self) -> None:
@typed_pos_args('foo', str, varargs=str, min_varargs=1)
@TypedArgs(
'foo',
pos_types=[STR_PARG],
var_types=STR_VARG.evolve(min_args=1),
)
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string'], None)
_(None, mock.Mock(), ['string'], {})
self.assertEqual(str(cm.exception), '"foo" takes at least 2 arguments, but got 1.')
def test_typed_pos_args_min_and_max_varargs_exceeded(self) -> None:
@typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2)
@TypedArgs(
'foo',
pos_types=[STR_PARG],
var_types=STR_VARG.evolve(min_args=1, max_args=2),
)
def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', 'var', 'args', 'bar'], None)
_(None, mock.Mock(), ['string', 'var', 'args', 'bar'], {})
self.assertEqual(str(cm.exception), '"foo" takes between 2 and 3 arguments, but got 4.')
def test_typed_pos_args_min_and_max_varargs_not_met(self) -> None:
@typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2)
@TypedArgs(
'foo',
pos_types=[STR_PARG],
var_types=STR_VARG.evolve(min_args=1, max_args=2),
)
def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string'], None)
_(None, mock.Mock(), ['string'], {})
self.assertEqual(str(cm.exception), '"foo" takes between 2 and 3 arguments, but got 1.')
def test_typed_pos_args_variadic_and_optional(self) -> None:
@typed_pos_args('foo', str, optargs=[str], varargs=str, min_varargs=0)
@TypedArgs(
'foo',
pos_types=[STR_PARG],
opt_types=[STR_OARG],
var_types=STR_VARG.evolve(min_args=0),
)
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(AssertionError) as cm:
_(None, mock.Mock(), ['string'], None)
_(None, mock.Mock(), ['string'], {})
self.assertEqual(
str(cm.exception),
'varargs and optargs not supported together as this would be ambiguous')
'Cannot use optional arguments and variadic arguments together due to ambiguity')
def test_typed_pos_args_min_optargs_not_met(self) -> None:
@typed_pos_args('foo', str, str, optargs=[str])
@TypedArgs(
'foo',
pos_types=[STR_PARG, STR_PARG],
opt_types=[STR_OARG],
)
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string'], None)
_(None, mock.Mock(), ['string'], {})
self.assertEqual(str(cm.exception), '"foo" takes at least 2 arguments, but got 1.')
def test_typed_pos_args_min_optargs_max_exceeded(self) -> None:
@typed_pos_args('foo', str, optargs=[str])
@TypedArgs(
'foo',
pos_types=[STR_PARG],
opt_types=[STR_OARG],
)
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
self.fail('Should not be reachable')
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', '1', '2'], None)
_(None, mock.Mock(), ['string', '1', '2'], {})
self.assertEqual(str(cm.exception), '"foo" takes at most 2 arguments, but got 3.')
def test_typed_pos_args_optargs_not_given(self) -> None:
@typed_pos_args('foo', str, optargs=[str])
@TypedArgs(
'foo',
pos_types=[STR_PARG],
opt_types=[STR_OARG],
)
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
self.assertEqual(len(args), 2)
self.assertIsInstance(args[0], str)
self.assertEqual(args[0], 'string')
self.assertIsNone(args[1])
_(None, mock.Mock(), ['string'], None)
_(None, mock.Mock(), ['string'], {})
def test_typed_pos_args_optargs_some_given(self) -> None:
@typed_pos_args('foo', str, optargs=[str, int])
@TypedArgs(
'foo',
pos_types=[STR_PARG],
opt_types=[STR_OARG, INT_OARG],
)
def _(obj, node, args: T.Tuple[str, T.Optional[str], T.Optional[int]], kwargs) -> None:
self.assertEqual(len(args), 3)
self.assertIsInstance(args[0], str)
@@ -1685,17 +1750,334 @@ Thread model: posix'''), '21.9.0')
self.assertEqual(args[1], '1')
self.assertIsNone(args[2])
_(None, mock.Mock(), ['string', '1'], None)
_(None, mock.Mock(), ['string', '1'], {})
def test_typed_pos_args_optargs_all_given(self) -> None:
@typed_pos_args('foo', str, optargs=[str])
@TypedArgs(
'foo',
pos_types=[STR_PARG],
opt_types=[STR_OARG],
)
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
self.assertEqual(len(args), 2)
self.assertIsInstance(args[0], str)
self.assertEqual(args[0], 'string')
self.assertIsInstance(args[1], str)
_(None, mock.Mock(), ['string', '1'], None)
_(None, mock.Mock(), ['string', '1'], {})
def test_typed_pos_args_since(self) -> None:
@TypedArgs(
'testfunc',
pos_types=[
STR_PARG.evolve(
since='1.0',
since_message='It\'s awesome, use it',
deprecated='2.0',
deprecated_message='It\'s terrible, don\'t use it'),
],
)
def _(obj, node, args: tuple[str], kwargs: dict) -> None:
self.assertIsInstance(args[0], str)
self.assertEqual(args[0], 'foo')
with self.subTest('use before available'), \
mock.patch('sys.stdout', io.StringIO()) as out, \
mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=0.1'])}):
# With Meson 0.1 it should trigger the "introduced" warning but not the "deprecated" warning
_(None, mock.Mock(subproject=''), ['foo'], {})
self.assertRegex(out.getvalue(), r'WARNING:.*introduced.*positional argument "\d" in testfunc. It\'s awesome, use it')
self.assertNotRegex(out.getvalue(), r'WARNING:.*deprecated.*argument "\d" in testfunc. It\'s terrible, don\'t use it')
with self.subTest('no warnings should be triggered'), \
mock.patch('sys.stdout', io.StringIO()) as out, \
mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=1.5'])}):
# With Meson 1.5 it shouldn't trigger any warning
_(None, mock.Mock(subproject=''), ['foo'], {})
self.assertNotRegex(out.getvalue(), r'WARNING:.*')
with self.subTest('use after deprecated'), \
mock.patch('sys.stdout', io.StringIO()) as out, \
mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=2.0'])}):
# With Meson 2.0 it should trigger the "deprecated" warning but not the "introduced" warning
_(None, mock.Mock(subproject=''), ['foo'], {})
self.assertRegex(out.getvalue(), r'WARNING:.*deprecated.*positional argument "\d" in testfunc. It\'s terrible, don\'t use it')
self.assertNotRegex(out.getvalue(), r'WARNING:.*introduced.*positional argument "\d" in testfunc. It\'s awesome, use it')
def test_typed_pos_args_optional_since(self) -> None:
@TypedArgs(
'testfunc',
opt_types=[
STR_OARG.evolve(
default='foo',
optional_since='1.0',
optional_since_message='Woo!',
)
],
)
def _(obj, node, args: tuple[str], kwargs: dict) -> None:
self.assertIsInstance(args[0], str)
self.assertEqual(args[0], 'foo')
with self.subTest('use before available'), \
mock.patch('sys.stdout', io.StringIO()) as out, \
mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=0.1'])}):
# With Meson 0.1 it should trigger the "introduced" warning but not the "deprecated" warning
_(None, mock.Mock(subproject=''), [], {})
self.assertIn('WARNING: Project targets \'>= 0.1\' but uses feature introduced in \'1.0\': positional argument "0" in testfunc as optional. Woo!',
out.getvalue())
with self.subTest('no warnings should be triggered'), \
mock.patch('sys.stdout', io.StringIO()) as out, \
mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=1.5'])}):
# With Meson 1.5 it shouldn't trigger any warning
_(None, mock.Mock(subproject=''), [], {})
self.assertNotRegex(out.getvalue(), r'WARNING:.*')
with self.subTest('use after deprecated'), \
mock.patch('sys.stdout', io.StringIO()) as out, \
mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=2.0'])}):
# With Meson 2.0 it should trigger the "deprecated" warning but not the "introduced" warning
_(None, mock.Mock(subproject=''), [], {})
self.assertNotIn('WARNING: Project targets \'>= 0.1\' but uses feature introduced in \'1.0\': positional argument "0" in testfunc as optional. Woo!',
out.getvalue())
def test_typed_pos_args_container(self) -> None:
@TypedArgs(
'testfunc',
pos_types=[PosArgInfo(ContainerTypeInfo(list, str))],
)
def _(obj, node, args: T.Tuple[list[str]], kwargs: dict) -> None:
self.assertEqual(args[0], ['str'])
with self.subTest('valid'):
_(None, mock.Mock(), [['str']], {})
with self.subTest('invalid'):
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), [0], {})
self.assertEqual(str(cm.exception), '"testfunc" positional argument "1" was of type "int" but should have been "array[str]"')
def test_typed_pos_args_contained(self) -> None:
@TypedArgs(
'testfunc',
pos_types=[PosArgInfo(ContainerTypeInfo(dict, str))],
)
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.Dict[str, str]]) -> None:
self.assertEqual(args[0], {'key': 'value'})
with self.subTest('valid'):
_(None, mock.Mock(), [{'key': 'value'}], {})
with self.subTest('invalid'):
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), [{'key': 0}], {})
self.assertEqual(str(cm.exception), '"testfunc" positional argument "1" was of type "dict[int]" but should have been "dict[str]"')
def test_typed_pos_args_validator(self) -> None:
@TypedArgs(
'testfunc',
pos_types=[
STR_PARG.evolve(validator=lambda x: 'invalid!' if x != 'foo' else None)
]
)
def _(obj, node, args: T.Tuple[str], kwargs: dict) -> None:
pass
# Should be valid
_(None, mock.Mock(), ['foo'], {})
with self.assertRaises(MesonException) as cm:
_(None, mock.Mock(), ['bar'], {})
self.assertEqual(str(cm.exception), "\"testfunc\" positional argument \"1\" invalid!")
def test_typed_pos_args_convertor(self) -> None:
@TypedArgs(
'testfunc',
pos_types=[
BOOL_PARG.evolve(convertor=lambda n: MachineChoice.BUILD if n else MachineChoice.HOST)
],
)
def _(obj, node, args: T.Tuple[MachineChoice], kwargs: dict) -> None:
assert isinstance(args[0], MachineChoice)
_(None, mock.Mock(), [True], {})
@mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=1.0'])})
def test_typed_pos_args_since_values(self) -> None:
@TypedArgs(
'testfunc',
pos_types=[
PosArgInfo(ContainerTypeInfo(list, str), listify=True, deprecated_values={'foo': '0.9'}, since_values={'bar': '1.1'}),
]
)
def _(obj, node, args: T.Tuple[str], kwargs: dict) -> None:
pass
with self.subTest('deprecated array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [['foo']], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*deprecated since '0.9': "testfunc" positional argument "1" value "foo".*""")
with self.subTest('new array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [['bar']], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*introduced in '1.1': "testfunc" positional argument "1" value "bar".*""")
with self.subTest('deprecated array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), ['foo'], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*deprecated since '0.9': "testfunc" positional argument "1" value "foo".*""")
with self.subTest('new array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), ['bar'], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*introduced in '1.1': "testfunc" positional argument "1" value "bar".*""")
@TypedArgs(
'testfunc',
pos_types=[
PosArgInfo(ContainerTypeInfo(dict, str), deprecated_values={'foo': '0.9', 'foo2': ('0.9', 'don\'t use it')}, since_values={'bar': '1.1', 'bar2': ('1.1', 'use this')}),
]
)
def _(obj, node, args: T.Tuple[str], kwargs: dict) -> None:
pass
with self.subTest('deprecated dict string value'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [{'foo': 'a'}], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*deprecated since '0.9': "testfunc" positional argument "1" value "foo".*""")
with self.subTest('deprecated dict string value with msg'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [{'foo2': 'a'}], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*deprecated since '0.9': "testfunc" positional argument "1" value "foo2" in dict keys. don't use it.*""")
with self.subTest('new dict string value'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [{'bar': 'a'}], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*introduced in '1.1': "testfunc" positional argument "1" value "bar".*""")
with self.subTest('new dict string value with msg'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [{'bar2': 'a'}], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*introduced in '1.1': "testfunc" positional argument "1" value "bar2" in dict keys. use this.*""")
@TypedArgs(
'testfunc',
pos_types=[
PosArgInfo(
(str, int, ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str), ContainerTypeInfo(list, int)),
since_values={str: '1.1', ContainerTypeInfo(list, str): '1.2', ContainerTypeInfo(dict, str): '1.3'},
deprecated_values={int: '0.8', ContainerTypeInfo(list, int): '0.9'},
),
]
)
def _(obj, node, args: T.Tuple[str], kwargs: dict) -> None:
pass
with self.subTest('new string type'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), ['foo'], {})
self.assertRegex(out.getvalue(), r"""WARNING: Project targets '>= 1.0'.*introduced in '1.1': "testfunc" positional argument "1" of type "str".*""")
with self.subTest('new array of string type'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [['foo']], {})
self.assertRegex(out.getvalue(), r"""WARNING: Project targets '>= 1.0'.*introduced in '1.2': "testfunc" positional argument "1" of type "array\[str\]".*""")
with self.subTest('new dict of string type'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [{'plop': 'foo'}], {})
self.assertRegex(out.getvalue(), r"""WARNING: Project targets '>= 1.0'.*introduced in '1.3': "testfunc" positional argument "1" of type "dict\[str\]".*""")
with self.subTest('deprecated int value'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [1], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*deprecated since '0.8': "testfunc" positional argument "1" of type "int".*""")
with self.subTest('deprecated array int value'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [[1]], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*deprecated since '0.9': "testfunc" positional argument "1" of type "array\[int\]".*""")
@TypedArgs(
'testfunc',
pos_types=[
PosArgInfo(
(ContainerTypeInfo(list, (str, int))),
listify=True,
since_values={ContainerTypeInfo(list, str): '1.1', ContainerTypeInfo(list, int): '1.2'},
),
]
)
def _(obj, node, args: T.Tuple[str], kwargs: dict) -> None:
pass
with self.subTest('new list[str] value'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [['foo', 42]], {})
self.assertRegex(out.getvalue(), r"""WARNING: Project targets '>= 1.0'.*introduced in '1.1': "testfunc" positional argument "1" of type "array\[str\]".*""")
self.assertRegex(out.getvalue(), r"""WARNING: Project targets '>= 1.0'.*introduced in '1.2': "testfunc" positional argument "1" of type "array\[int\]".*""")
@TypedArgs(
'testfunc',
pos_types=[
PosArgInfo((bool, str, NoneType), deprecated_values={False: '0.9'}),
]
)
def _(obj, node, args: T.Tuple[str], kwargs: dict) -> None:
pass
with self.subTest('non string union'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [False], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*deprecated since '0.9': "testfunc" positional argument "1" value "False".*""")
@TypedArgs(
'testfunc',
pos_types=[
PosArgInfo(
(str, type(None)),
validator=in_set_validator({'clean', 'build', 'rebuild', 'deprecated', 'since'}),
deprecated_values={'deprecated': '1.0'},
since_values={'since': '1.1'},
),
]
)
def _(obj, node, args: T.Tuple[str], kwargs: dict) -> None:
pass
with self.subTest('deprecated string union'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), ['deprecated'], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*deprecated since '1.0': "testfunc" positional argument "1" value "deprecated".*""")
with self.subTest('new string union'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), ['since'], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*introduced in '1.1': "testfunc" positional argument "1" value "since".*""")
@TypedArgs(
'testfunc',
pos_types=[
PosArgInfo(
(ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str)),
since_values={list: '1.9'},
),
]
)
def _(obj, node, args: T.Tuple[str], kwargs: dict) -> None:
pass
with self.subTest('new container'), mock.patch('sys.stdout', io.StringIO()) as out:
_(None, mock.Mock(subproject=''), [['a=b']], {})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '>= 1.0'.*introduced in '1.9': "testfunc" positional argument "1" of type "list".*""")
def test_typed_pos_args_default(self) -> None:
@TypedArgs(
'testfunc',
opt_types=[STR_OARG.evolve(default='foo')],
)
def _(obj, node, args: T.Tuple[str], kwargs: dict) -> None:
self.assertEqual(len(args), 1)
self.assertIsInstance(args[0], str)
self.assertEqual(args[0], 'foo')
with self.subTest('default object before available'), \
mock.patch('sys.stdout', io.StringIO()) as out, \
mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=1.0'])}):
_(None, mock.Mock(subproject=''), [DefaultObject()], {})
self.assertRegex(out.getvalue(), r'WARNING: Project targets \'>= 1.0\' but uses feature introduced in \'1.13.0\': default\(\) object for optional')
with self.subTest('default object after available'), \
mock.patch('sys.stdout', io.StringIO()) as out, \
mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=1.13'])}):
_(None, mock.Mock(subproject=''), [DefaultObject()], {})
self.assertNotRegex(out.getvalue(), r'WARNING: Project targets \'>= 1.0\' but uses feature introduced in \'1.13.0\': default\(\) object for optional')
def test_typed_kwarg_basic(self) -> None:
@TypedArgs(
@@ -1853,10 +2235,10 @@ Thread model: posix'''), '21.9.0')
pass
# Should be valid
_(None, mock.Mock(), tuple(), dict(input='foo'))
_(None, mock.Mock(), [], dict(input='foo'))
with self.assertRaises(MesonException) as cm:
_(None, mock.Mock(), tuple(), dict(input='bar'))
_(None, mock.Mock(), [], dict(input='bar'))
self.assertEqual(str(cm.exception), "\"testfunc\" keyword argument \"input\" invalid!")
def test_typed_kwarg_convertor(self) -> None:
@@ -1867,7 +2249,7 @@ Thread model: posix'''), '21.9.0')
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, MachineChoice]) -> None:
assert isinstance(kwargs['native'], MachineChoice)
_(None, mock.Mock(), tuple(), dict(native=True))
_(None, mock.Mock(), [], dict(native=True))
@mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': version_check_to_range(['>=1.0'])})
def test_typed_kwarg_since_values(self) -> None:
@@ -2039,6 +2421,7 @@ Thread model: posix'''), '21.9.0')
def test_typed_kwarg_container_in_tuple(self) -> None:
@TypedArgs(
'testfunc',
pos_types=[PosArgInfo(object)],
kw_types=[KwargInfo('input', (str, ContainerTypeInfo(list, str)))],
)
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: