diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py index f997111ecf39..b82a4130cbe8 100644 --- a/mesonbuild/interpreterbase/decorators.py +++ b/mesonbuild/interpreterbase/decorators.py @@ -290,13 +290,19 @@ class KwargInfo(T.Generic[_T]): different type. This is intended for cases such as the meson DSL using a string, but the implementation using an Enum. This should not do validation, just converstion. + :param deprecated_values: a dictionary mapping a value to the version of + meson it was deprecated in. + :param since: a dictionary mapping a value to the version of meson it was + added in. """ 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, + since_values: T.Optional[T.Dict[str, str]] = None, deprecated: T.Optional[str] = None, + deprecated_values: T.Optional[T.Dict[str, str]] = None, validator: T.Optional[T.Callable[[_T], T.Optional[str]]] = None, convertor: T.Optional[T.Callable[[_T], TYPE_nvar]] = None): self.name = name @@ -304,8 +310,10 @@ def __init__(self, name: str, types: T.Union[T.Type[_T], T.Tuple[T.Type[_T], ... self.required = required self.listify = listify self.default = default + self.since_values = since_values self.since = since self.deprecated = deprecated + self.deprecated_values = deprecated_values self.validator = validator self.convertor = convertor @@ -368,6 +376,28 @@ def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: msg = info.validator(value) if msg is not None: raise InvalidArguments(f'{name} keyword argument "{info.name}" {msg}') + + warn: bool + if info.deprecated_values is not None: + for n, version in info.deprecated_values.items(): + if isinstance(value, (dict, list)): + warn = n in value + else: + warn = n == value + + if warn: + FeatureDeprecated.single_use(f'"{name}" keyword argument "{info.name}" value "{n}"', version, subproject) + + if info.since_values is not None: + for n, version in info.since_values.items(): + if isinstance(value, (dict, list)): + warn = n in value + else: + warn = n == value + + if warn: + FeatureNew.single_use(f'"{name}" keyword argument "{info.name}" value "{n}"', version, subproject) + 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 b43e8f05c7e8..d17c24338556 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -1655,6 +1655,45 @@ def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, MachineChoice]) -> None: _(None, mock.Mock(), tuple(), dict(native=True)) + @mock.patch.dict(mesonbuild.mesonlib.project_meson_versions, {'': '1.0'}) + def test_typed_kwarg_since_values(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', ContainerTypeInfo(list, str), listify=True, default=[], deprecated_values={'foo': '0.9'}, since_values={'bar': '1.1'}), + KwargInfo('output', ContainerTypeInfo(dict, str), default={}, deprecated_values={'foo': '0.9'}, since_values={'bar': '1.1'}), + KwargInfo( + 'mode', str, + validator=lambda x: 'Should be one of "clean", "build", "rebuild"' if x not in {'clean', 'build', 'rebuild', 'deprecated', 'since'} else None, + deprecated_values={'deprecated': '1.0'}, + since_values={'since': '1.1'}), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: + pass + + with mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'input': ['foo']}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*deprecated since '0.9': "testfunc" keyword argument "input" value "foo".*""") + + with mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'input': ['bar']}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*introduced in '1.1': "testfunc" keyword argument "input" value "bar".*""") + + with mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'output': {'foo': 'a'}}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*deprecated since '0.9': "testfunc" keyword argument "output" value "foo".*""") + + with mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'output': {'bar': 'b'}}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*introduced in '1.1': "testfunc" keyword argument "output" value "bar".*""") + + with mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'mode': 'deprecated'}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*deprecated since '1.0': "testfunc" keyword argument "mode" value "deprecated".*""") + + with mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'mode': 'since'}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*introduced in '1.1': "testfunc" keyword argument "mode" value "since".*""") + @unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release') class DataTests(unittest.TestCase):