interpreterbase: Add validator keyword argument to typed_kwargs

This attribute is a callable that returns a string if the value is
invalid, otherwise None. This intended for cases like the `install_*`
function's `install_mode` paramater, which is either an int or the
string "preserve", which allows us to do nice things like:

```python

class Kwargs(TypedDict):

    install_mode: T.Union[int, T.Literal['preserve']]

@typed_kwargs(
  'foo', KwargInfo('install_mode', ...,
  validator=lambda x: None if isinstance(x, int) or x == 'preserve' else 'must be the literal "preserve"),
)
def install_data(self, node, args, kwargs: 'Kwargs'):
  ...
```

In this case mypy *knows* that the string is preserve, as do we, and we
can simply do tests like:
```python
if kwargs['install_mode'] == 'preserve':
   ...
else:
   # this is an int
```
pull/8853/head
Dylan Baker 4 years ago committed by Daniel Mensinger
parent 5d81392c67
commit fb385728df
  1. 14
      mesonbuild/interpreterbase.py
  2. 15
      run_unittests.py

@ -424,12 +424,18 @@ class KwargInfo(T.Generic[_T]):
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 deprecated: Meson version in which this argument has been deprecated. defaults to None
: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
"""
def __init__(self, name: str, types: T.Union[T.Type[_T], T.Tuple[T.Type[_T], ...], ContainerTypeInfo],
*, required: bool = False, listify: bool = False,
default: T.Optional[_T] = None,
since: T.Optional[str] = None, deprecated: T.Optional[str] = None):
since: T.Optional[str] = None,
deprecated: T.Optional[str] = None,
validator: T.Optional[T.Callable[[_T], T.Optional[str]]] = None):
self.name = name
self.types = types
self.required = required
@ -437,6 +443,7 @@ class KwargInfo(T.Generic[_T]):
self.default = default
self.since = since
self.deprecated = deprecated
self.validator = validator
def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]:
@ -492,6 +499,11 @@ def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]:
else:
shouldbe = f'"{info.types.__name__}"'
raise InvalidArguments(f'{name} keyword argument "{info.name}"" was of type "{type(value).__name__}" 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}')
elif info.required:
raise InvalidArguments(f'{name} is missing required keyword argument "{info.name}"')
else:

@ -1629,6 +1629,21 @@ class InternalTests(unittest.TestCase):
self.assertRegex(out.getvalue(), r'WARNING:.*deprecated.*input arg in testfunc')
self.assertNotRegex(out.getvalue(), r'WARNING:.*introduced.*input arg in testfunc')
def test_typed_kwarg_validator(self) -> None:
@typed_kwargs(
'testfunc',
KwargInfo('input', str, validator=lambda x: 'invalid!' if x != 'foo' else None)
)
def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
pass
# Should be valid
_(None, mock.Mock(), tuple(), dict(input='foo'))
with self.assertRaises(MesonException) as cm:
_(None, mock.Mock(), tuple(), dict(input='bar'))
self.assertEqual(str(cm.exception), "testfunc keyword argument \"input\" invalid!")
@unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release')
class DataTests(unittest.TestCase):

Loading…
Cancel
Save