aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Claessens <xavier.claessens@collabora.com>2021-08-27 11:32:42 -0400
committerXavier Claessens <xclaesse@gmail.com>2021-10-09 18:13:34 -0400
commit709d151eb944d764b35008ca3275b02bd16a765d (patch)
treeed731fc59be4131eb3b8f2cc9e5951b4899f07a6
parent329d111709ab5c5140f75f29c7176c9546de5770 (diff)
downloadmeson-709d151eb944d764b35008ca3275b02bd16a765d.zip
meson-709d151eb944d764b35008ca3275b02bd16a765d.tar.gz
meson-709d151eb944d764b35008ca3275b02bd16a765d.tar.bz2
typed_kwargs: Fix when ContainerTypeInfo is used in a tuple
info.types could be a tuple like (str, ContainerTypeInfo()). That means we have to check types one by one and only print error if none of them matched. Also fix the case when default value is None for a container type, it should leave the value to None to be able to distinguish between unset and empty list.
-rw-r--r--mesonbuild/interpreterbase/decorators.py87
-rw-r--r--unittests/internaltests.py41
2 files changed, 89 insertions, 39 deletions
diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py
index 4c8d824..54a4960 100644
--- a/mesonbuild/interpreterbase/decorators.py
+++ b/mesonbuild/interpreterbase/decorators.py
@@ -22,6 +22,7 @@ from ._unholder import _unholder
from functools import wraps
import abc
import itertools
+import copy
import typing as T
if T.TYPE_CHECKING:
from .. import mparser
@@ -302,28 +303,40 @@ class ContainerTypeInfo:
self.pairs = pairs
self.allow_empty = allow_empty
- def check(self, value: T.Any) -> T.Optional[str]:
+ def check(self, value: T.Any) -> bool:
"""Check that a value is valid.
:param value: A value to check
- :return: If there is an error then a string message, otherwise None
+ :return: True if it is valid, False otherwise
"""
if not isinstance(value, self.container):
- return f'container type was "{type(value).__name__}", but should have been "{self.container.__name__}"'
+ return False
iter_ = iter(value.values()) if isinstance(value, dict) else iter(value)
for each in iter_:
if not isinstance(each, self.contains):
- if isinstance(self.contains, tuple):
- shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in self.contains))
- else:
- shouldbe = f'"{self.contains.__name__}"'
- return f'contained a value of type "{type(each).__name__}" but should have been {shouldbe}'
+ return False
if self.pairs and len(value) % 2 != 0:
- return 'container should be of even length, but is not'
+ return False
if not value and not self.allow_empty:
- return 'container is empty, but not allowed to be'
- return None
+ return False
+ return True
+
+ def description(self) -> str:
+ """Human readable description of this container type.
+ :return: string to be printed
+ """
+ container = 'dict' if self.container is dict else 'list'
+ if isinstance(self.contains, tuple):
+ contains = ','.join([t.__name__ for t in self.contains])
+ else:
+ contains = self.contains.__name__
+ s = f'{container}[{contains}]'
+ if self.pairs:
+ s += ' that has even size'
+ if not self.allow_empty:
+ s += ' that cannot be empty'
+ return s
_T = T.TypeVar('_T')
@@ -365,8 +378,8 @@ class KwargInfo(T.Generic[_T]):
:param not_set_warning: A warning messsage that is logged if the kwarg is not
set by the user.
"""
-
- def __init__(self, name: str, types: T.Union[T.Type[_T], T.Tuple[T.Type[_T], ...], ContainerTypeInfo],
+ def __init__(self, name: str,
+ types: T.Union[T.Type[_T], T.Tuple[T.Union[T.Type[_T], ContainerTypeInfo], ...], ContainerTypeInfo],
*, required: bool = False, listify: bool = False,
default: T.Optional[_T] = None,
since: T.Optional[str] = None,
@@ -456,6 +469,25 @@ def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]:
raise InvalidArguments(f'{name} got unknown keyword arguments {ustr}')
for info in types:
+ types_tuple = info.types if isinstance(info.types, tuple) else (info.types,)
+ def check_value_type(value: T.Any) -> bool:
+ for t in types_tuple:
+ if isinstance(t, ContainerTypeInfo):
+ if t.check(value):
+ return True
+ elif isinstance(value, t):
+ return True
+ return False
+ def types_description() -> str:
+ candidates = []
+ for t in types_tuple:
+ if isinstance(t, ContainerTypeInfo):
+ candidates.append(t.description())
+ else:
+ candidates.append(t.__name__)
+ shouldbe = 'one of: ' if len(candidates) > 1 else ''
+ shouldbe += ', '.join(candidates)
+ return shouldbe
value = kwargs.get(info.name)
if value is not None:
if info.since:
@@ -466,17 +498,9 @@ def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]:
FeatureDeprecated.single_use(feature_name, info.deprecated, subproject)
if info.listify:
kwargs[info.name] = value = mesonlib.listify(value)
- if isinstance(info.types, ContainerTypeInfo):
- msg = info.types.check(value)
- if msg is not None:
- raise InvalidArguments(f'{name} keyword argument "{info.name}" {msg}')
- else:
- if not isinstance(value, info.types):
- if isinstance(info.types, tuple):
- shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in info.types))
- 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 not check_value_type(value):
+ shouldbe = types_description()
+ raise InvalidArguments(f'{name} keyword argument {info.name!r} was of type {type(value).__name__!r} but should have been {shouldbe}')
if info.validator is not None:
msg = info.validator(value)
@@ -509,17 +533,10 @@ def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]:
else:
# set the value to the default, this ensuring all kwargs are present
# This both simplifies the typing checking and the usage
- # Create a shallow copy of the container (and do a type
- # conversion if necessary). This allows mutable types to
- # be used safely as default values
- if isinstance(info.types, ContainerTypeInfo):
- assert isinstance(info.default, info.types.container), f'In function {name} default value of {info.name} is not a valid type, got {type(info.default)}, expected {info.types.container}[{info.types.contains}]'
- for item in info.default:
- assert isinstance(item, info.types.contains), f'In function {name} default value of {info.name}, container has invalid value of {item}, which is of type {type(item)}, but should be {info.types.contains}'
- kwargs[info.name] = info.types.container(info.default)
- else:
- assert isinstance(info.default, info.types), f'In funcion {name} default value of {info.name} is not a valid type, got {type(info.default)} expected {info.types}'
- kwargs[info.name] = info.default
+ assert check_value_type(info.default), f'In funcion {name} default value of {info.name} is not a valid type, got {type(info.default)} expected {types_description()}'
+ # Create a shallow copy of the container. This allows mutable
+ # types to be used safely as default values
+ kwargs[info.name] = copy.copy(info.default)
if info.not_set_warning:
mlog.warning(info.not_set_warning)
diff --git a/unittests/internaltests.py b/unittests/internaltests.py
index 957f180..cb50f37 100644
--- a/unittests/internaltests.py
+++ b/unittests/internaltests.py
@@ -42,7 +42,7 @@ from mesonbuild.mesonlib import (
LibType, MachineChoice, PerMachine, Version, is_windows, is_osx,
is_cygwin, is_openbsd, search_version, MesonException, OptionKey,
)
-from mesonbuild.interpreter.type_checking import in_set_validator
+from mesonbuild.interpreter.type_checking import in_set_validator, NoneType
from mesonbuild.dependencies import PkgConfigDependency
from mesonbuild.programs import ExternalProgram
import mesonbuild.modules.pkgconfig
@@ -1266,7 +1266,7 @@ class InternalTests(unittest.TestCase):
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), [], {'input': {}})
- self.assertEqual(str(cm.exception), 'testfunc keyword argument "input" container type was "dict", but should have been "list"')
+ self.assertEqual(str(cm.exception), "testfunc keyword argument 'input' was of type 'dict' but should have been list[str]")
def test_typed_kwarg_contained_invalid(self) -> None:
@typed_kwargs(
@@ -1278,7 +1278,7 @@ class InternalTests(unittest.TestCase):
with self.assertRaises(InvalidArguments) as cm:
_(None, mock.Mock(), [], {'input': {'key': 1}})
- self.assertEqual(str(cm.exception), 'testfunc keyword argument "input" contained a value of type "int" but should have been "str"')
+ self.assertEqual(str(cm.exception), "testfunc keyword argument 'input' was of type 'dict' but should have been dict[str]")
def test_typed_kwarg_container_listify(self) -> None:
@typed_kwargs(
@@ -1313,7 +1313,7 @@ class InternalTests(unittest.TestCase):
with self.assertRaises(MesonException) as cm:
_(None, mock.Mock(), [], {'input': ['a']})
- self.assertEqual(str(cm.exception), "testfunc keyword argument \"input\" container should be of even length, but is not")
+ self.assertEqual(str(cm.exception), "testfunc keyword argument 'input' was of type 'list' but should have been list[str] that has even size")
@mock.patch.dict(mesonbuild.mesonlib.project_meson_versions, {})
def test_typed_kwarg_since(self) -> None:
@@ -1425,6 +1425,39 @@ class InternalTests(unittest.TestCase):
self.assertEqual(k.default, 'foo')
self.assertEqual(v.default, 'bar')
+ def test_typed_kwarg_default_type(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('no_default', (str, ContainerTypeInfo(list, str), NoneType)),
+ KwargInfo('str_default', (str, ContainerTypeInfo(list, str)), default=''),
+ KwargInfo('list_default', (str, ContainerTypeInfo(list, str)), default=['']),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ self.assertEqual(kwargs['no_default'], None)
+ self.assertEqual(kwargs['str_default'], '')
+ self.assertEqual(kwargs['list_default'], [''])
+ _(None, mock.Mock(), [], {})
+
+ def test_typed_kwarg_invalid_default_type(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('invalid_default', (str, ContainerTypeInfo(list, str), NoneType), default=42),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ pass
+ self.assertRaises(AssertionError, _, None, mock.Mock(), [], {})
+
+ def test_typed_kwarg_container_in_tuple(self) -> None:
+ @typed_kwargs(
+ 'testfunc',
+ KwargInfo('input', (str, ContainerTypeInfo(list, str))),
+ )
+ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
+ self.assertEqual(kwargs['input'], args[0])
+ _(None, mock.Mock(), [''], {'input': ''})
+ _(None, mock.Mock(), [['']], {'input': ['']})
+ self.assertRaises(InvalidArguments, _, None, mock.Mock(), [], {'input': 42})
+
def test_detect_cpu_family(self) -> None:
"""Test the various cpu familes that we detect and normalize.