interpreterbase: Add support for optional arguments to typed_pos_args

This allows representing functions like assert(), which take optional
positional arguments, which are not variadic. More importnatly you can
represent a function like (* means optional, but possitional):
```txt
func(str, *int, *str)
```

typed_pos_args will check that all of your types are correct, and if not
provide None, which allow simplifying a number of implementation details
pull/8105/merge
Dylan Baker 4 years ago committed by Xavier Claessens
parent 978eeddab8
commit 2650977c38
  1. 40
      mesonbuild/interpreterbase.py
  2. 60
      run_unittests.py

@ -232,9 +232,27 @@ class permittedKwargs:
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 aguments, 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 paramters are taken.
allows replacing this:
```python
def func(self, node, args, kwargs):
@ -268,9 +286,12 @@ def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]],
assert isinstance(args, list), args
assert max_varargs >= 0, 'max_varrags cannot be negative'
assert min_varargs >= 0, 'min_varrags 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:
num_types += min_varargs
@ -278,10 +299,19 @@ def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]],
raise InvalidArguments(f'{name} takes at least {num_types} arguments, but got {num_args}.')
elif max_varargs != 0 and (num_args < num_types or num_args > num_types + max_varargs - min_varargs):
raise InvalidArguments(f'{name} takes between {num_types} and {num_types + max_varargs - min_varargs} arguments, but got {len(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, types, fillvalue=varargs), start=1):
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_))
@ -298,11 +328,17 @@ def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]],
# 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 cas we need T.Tuple[int, str, float, T.Tuple[str, ...]]
# 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)

@ -1422,6 +1422,66 @@ class InternalTests(unittest.TestCase):
_(None, mock.Mock(), ['string'], None)
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)
def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
self.assertTrue(False) # should not be reachable
with self.assertRaises(AssertionError) as cm:
_(None, mock.Mock(), ['string'], None)
self.assertEqual(
str(cm.exception),
'varargs and optargs not supported together as this would be ambiguous')
def test_typed_pos_args_min_optargs_not_met(self) -> None:
@typed_pos_args('foo', str, str, optargs=[str])
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
self.assertTrue(False) # should not be reachable
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string'], None)
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])
def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
self.assertTrue(False) # should not be reachable
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), ['string', '1', '2'], None)
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])
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)
def test_typed_pos_args_optargs_some_given(self) -> None:
@typed_pos_args('foo', str, optargs=[str, int])
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)
self.assertEqual(args[0], 'string')
self.assertIsInstance(args[1], str)
self.assertEqual(args[1], '1')
self.assertIsNone(args[2])
_(None, mock.Mock(), ['string', '1'], None)
def test_typed_pos_args_optargs_all_given(self) -> None:
@typed_pos_args('foo', str, optargs=[str])
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)
@unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release')
class DataTests(unittest.TestCase):

Loading…
Cancel
Save