From b107171307505e1493f76b53ace7db1ac070e819 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 1 Jun 2021 15:48:23 -0700 Subject: interpreterbase: Allow safely using mutable default values with typed_kwargs It's really inconvenient to want a thing that is always a list, but not be able to provide a default value of a list because of mutation. To that end the typed_kwargs method now makes a shallow copy of the default when using a `ContainerTypeInfo` as the type. This mean that using a default of `[]` is perfectly safe. --- mesonbuild/interpreterbase.py | 15 +++++++++++++-- run_unittests.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py index 750101e..c887115 100644 --- a/mesonbuild/interpreterbase.py +++ b/mesonbuild/interpreterbase.py @@ -419,7 +419,9 @@ class KwargInfo(T.Generic[_T]): :param listify: If true, then the argument will be listified before being checked. This is useful for cases where the Meson DSL allows a scalar or a container, but internally we only want to work with containers - :param default: A default value to use if this isn't set. defaults to None + :param default: A default value to use if this isn't set. defaults to None, + this may be safely set to a mutable type, as long as that type does not + 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 """ @@ -444,6 +446,9 @@ def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]: information. For non-required values it sets the value to a default, which means the value will always be provided. + If type tyhpe is a :class:ContainerTypeInfo, then the default value will be + passed as an argument to the container initializer, making a shallow copy + :param name: the name of the function, including the object it's attached ot (if applicable) :param *types: KwargInfo entries for each keyword argument. @@ -491,7 +496,13 @@ 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 - kwargs[info.name] = info.default + # 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): + kwargs[info.name] = info.types.container(info.default) + else: + kwargs[info.name] = info.default return f(*wrapped_args, **wrapped_kwargs) return T.cast(TV_func, wrapper) diff --git a/run_unittests.py b/run_unittests.py index e278675..a0beb48 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -1574,6 +1574,17 @@ class InternalTests(unittest.TestCase): _(None, mock.Mock(), [], {'input': 'str'}) + def test_typed_kwarg_container_default_copy(self) -> None: + default: T.List[str] = [] + @typed_kwargs( + 'testfunc', + KwargInfo('input', ContainerTypeInfo(list, str), listify=True, default=default), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: + self.assertIsNot(kwargs['input'], default) + + _(None, mock.Mock(), [], {}) + def test_typed_kwarg_container_pairs(self) -> None: @typed_kwargs( 'testfunc', -- cgit v1.1