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, ...]], 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, 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]: min_varargs: int = 0, max_varargs: int = 0) -> T.Callable[..., T.Any]:
"""Decorator that types type checking of positional arguments. """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: allows replacing this:
```python ```python
def func(self, node, args, kwargs): 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 isinstance(args, list), args
assert max_varargs >= 0, 'max_varrags cannot be negative' assert max_varargs >= 0, 'max_varrags cannot be negative'
assert min_varargs >= 0, 'min_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_args = len(args)
num_types = len(types) num_types = len(types)
a_types = types
if varargs: if varargs:
num_types += min_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}.') 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): 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)}.') 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: elif num_args != num_types:
raise InvalidArguments(f'{name} takes exactly {num_types} arguments, but got {num_args}.') 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 not isinstance(arg, type_):
if isinstance(type_, tuple): if isinstance(type_, tuple):
shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in type_)) 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 # if we have varargs we need to split them into a separate
# tuple, as python's typing doesn't understand tuples with # tuple, as python's typing doesn't understand tuples with
# fixed elements and variadic elements, only one or the other. # 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)] pos = args[:len(types)]
var = list(args[len(types):]) var = list(args[len(types):])
pos.append(var) pos.append(var)
nargs[i] = tuple(pos) 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: else:
nargs[i] = tuple(args) nargs[i] = tuple(args)
return f(*nargs, **wrapped_kwargs) return f(*nargs, **wrapped_kwargs)

@ -1422,6 +1422,66 @@ class InternalTests(unittest.TestCase):
_(None, mock.Mock(), ['string'], None) _(None, mock.Mock(), ['string'], None)
self.assertEqual(str(cm.exception), 'foo takes between 2 and 3 arguments, but got 1.') 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') @unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release')
class DataTests(unittest.TestCase): class DataTests(unittest.TestCase):

Loading…
Cancel
Save