diff options
author | Dylan Baker <dylan@pnwbakers.com> | 2021-06-02 15:45:15 -0700 |
---|---|---|
committer | Daniel Mensinger <daniel@mensinger-ka.de> | 2021-06-08 11:00:55 +0200 |
commit | fb385728df5d14c929420c6fd0fb2958f274761d (patch) | |
tree | 349e686a476fec3406520b69a335f4db327bad83 | |
parent | 5d81392c67231fa68a67bdacf2a2764ef3e76da5 (diff) | |
download | meson-fb385728df5d14c929420c6fd0fb2958f274761d.zip meson-fb385728df5d14c929420c6fd0fb2958f274761d.tar.gz meson-fb385728df5d14c929420c6fd0fb2958f274761d.tar.bz2 |
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
```
-rw-r--r-- | mesonbuild/interpreterbase.py | 14 | ||||
-rwxr-xr-x | run_unittests.py | 15 |
2 files changed, 28 insertions, 1 deletions
diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py index 6e31202..136eb6e 100644 --- a/mesonbuild/interpreterbase.py +++ b/mesonbuild/interpreterbase.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: diff --git a/run_unittests.py b/run_unittests.py index 39b3496..9f66d9c 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -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): |