diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index 00f63e193..de7ba042f 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import re import typing as T @@ -19,11 +20,13 @@ from . import coredata from . import mesonlib from . import mparser from . import mlog -from .interpreterbase import FeatureNew, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo, permittedKwargs +from .interpreterbase import FeatureNew, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo +from .interpreter.type_checking import NoneType, in_set_validator if T.TYPE_CHECKING: from .interpreterbase import TYPE_var, TYPE_kwargs from .interpreterbase import SubProject - from typing_extensions import TypedDict + from typing_extensions import TypedDict, Literal + FuncOptionArgs = TypedDict('FuncOptionArgs', { 'type': str, 'description': str, @@ -34,13 +37,29 @@ if T.TYPE_CHECKING: 'max': T.Optional[int], 'deprecated': T.Union[bool, str, T.Dict[str, str], T.List[str]], }) - ParserArgs = TypedDict('ParserArgs', { - 'yield': bool, - 'choices': T.Optional[T.List[str]], - 'value': object, - 'min': T.Optional[int], - 'max': T.Optional[int], - }) + + class StringArgs(TypedDict): + value: str + + class BooleanArgs(TypedDict): + value: bool + + class ComboArgs(TypedDict): + value: str + choices: T.List[str] + + class IntegerArgs(TypedDict): + value: int + min: T.Optional[int] + max: T.Optional[int] + + class StringArrayArgs(TypedDict): + value: T.Optional[T.Union[str, T.List[str]]] + choices: T.List[str] + + class FeatureArgs(TypedDict): + value: Literal['enabled', 'disabled', 'auto'] + choices: T.List[str] class OptionException(mesonlib.MesonException): @@ -54,13 +73,14 @@ class OptionInterpreter: def __init__(self, subproject: 'SubProject') -> None: self.options: 'coredata.MutableKeyedOptionDictType' = {} self.subproject = subproject - self.option_types = {'string': self.string_parser, - 'boolean': self.boolean_parser, - 'combo': self.combo_parser, - 'integer': self.integer_parser, - 'array': self.string_array_parser, - 'feature': self.feature_parser, - } + self.option_types: T.Dict[str, T.Callable[..., coredata.UserOption]] = { + 'string': self.string_parser, + 'boolean': self.boolean_parser, + 'combo': self.combo_parser, + 'integer': self.integer_parser, + 'array': self.string_array_parser, + 'feature': self.feature_parser, + } def process(self, option_file: str) -> None: try: @@ -145,17 +165,25 @@ class OptionInterpreter: (posargs, kwargs) = self.reduce_arguments(node.args) self.func_option(posargs, kwargs) - @typed_kwargs('option', - KwargInfo('type', str, required=True), - KwargInfo('description', str, default=''), - KwargInfo('yield', bool, default=coredata.DEFAULT_YIELDING, since='0.45.0'), - KwargInfo('choices', (ContainerTypeInfo(list, str), type(None))), - KwargInfo('value', object), - KwargInfo('min', (int, type(None))), - KwargInfo('max', (int, type(None))), - KwargInfo('deprecated', (bool, str, ContainerTypeInfo(dict, str), ContainerTypeInfo(list, str)), - default=False, since='0.60.0') - ) + @typed_kwargs( + 'option', + KwargInfo( + 'type', + str, + required=True, + validator=in_set_validator({'string', 'boolean', 'integer', 'combo', 'array', 'feature'}) + ), + KwargInfo('description', str, default=''), + KwargInfo( + 'deprecated', + (bool, str, ContainerTypeInfo(dict, str), ContainerTypeInfo(list, str)), + default=False, + since='0.60.0', + feature_validator=lambda x: [FeatureNew('string value to "deprecated" keyword argument', '0.63.0')] if isinstance(x, str) else [] + ), + KwargInfo('yield', bool, default=coredata.DEFAULT_YIELDING, since='0.45.0'), + allow_unknown=True, + ) @typed_pos_args('option', str) def func_option(self, args: T.Tuple[str], kwargs: 'FuncOptionArgs') -> None: opt_name = args[0] @@ -166,60 +194,81 @@ class OptionInterpreter: raise OptionException('Option name %s is reserved.' % opt_name) opt_type = kwargs['type'] - parser = self.option_types.get(opt_type) - if not parser: - raise OptionException(f'Unknown type {opt_type}.') + parser = self.option_types[opt_type] description = kwargs['description'] or opt_name - # Only keep in kwargs arguments that are used by option type's parser - # because they use @permittedKwargs(). - known_parser_kwargs = {'value', 'choices', 'yield', 'min', 'max'} - parser_kwargs = {k: v for k, v in kwargs.items() if k in known_parser_kwargs and v is not None} - opt = parser(description, T.cast('ParserArgs', parser_kwargs)) + # Drop the arguments we've already consumed + n_kwargs = {k: v for k, v in kwargs.items() + if k not in {'type', 'description', 'deprecated', 'yield'}} + + opt = parser(description, kwargs['yield'], n_kwargs) opt.deprecated = kwargs['deprecated'] - if isinstance(opt.deprecated, str): - FeatureNew.single_use('String value to "deprecated" keyword argument', '0.63.0', self.subproject) if key in self.options: mlog.deprecation(f'Option {opt_name} already exists.') self.options[key] = opt - @permittedKwargs({'value', 'yield'}) - def string_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - value = kwargs.get('value', '') - return coredata.UserStringOption(description, value, kwargs['yield']) - - @permittedKwargs({'value', 'yield'}) - def boolean_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - value = kwargs.get('value', True) - return coredata.UserBooleanOption(description, value, kwargs['yield']) - - @permittedKwargs({'value', 'yield', 'choices'}) - def combo_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - choices = kwargs.get('choices') - if not choices: - raise OptionException('Combo option missing "choices" keyword.') - value = kwargs.get('value', choices[0]) - return coredata.UserComboOption(description, choices, value, kwargs['yield']) - - @permittedKwargs({'value', 'min', 'max', 'yield'}) - def integer_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - value = kwargs.get('value') + @typed_kwargs( + 'string option', + KwargInfo('value', str, default=''), + ) + def string_parser(self, description: str, yield_: bool, kwargs: StringArgs) -> coredata.UserOption: + return coredata.UserStringOption(description, kwargs['value'], yield_) + + @typed_kwargs( + 'boolean option', + KwargInfo( + 'value', + (bool, str), + default=True, + validator=lambda x: None if isinstance(x, bool) or x in {'true', 'false'} else 'boolean options must have boolean values', + ), + ) + def boolean_parser(self, description: str, yield_: bool, kwargs: BooleanArgs) -> coredata.UserOption: + return coredata.UserBooleanOption(description, kwargs['value'], yield_) + + @typed_kwargs( + 'combo option', + KwargInfo('value', (str, NoneType)), + KwargInfo('choices', ContainerTypeInfo(list, str, allow_empty=False), required=True), + ) + def combo_parser(self, description: str, kwargs: ComboArgs, yield_: bool) -> coredata.UserOption: + choices = kwargs['choices'] + value = kwargs['value'] if value is None: - raise OptionException('Integer option must contain value argument.') - inttuple = (kwargs.get('min'), kwargs.get('max'), value) - return coredata.UserIntegerOption(description, inttuple, kwargs['yield']) - - @permittedKwargs({'value', 'yield', 'choices'}) - def string_array_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - choices = kwargs.get('choices', []) - value = kwargs.get('value', choices) - if not isinstance(value, list): - raise OptionException('Array choices must be passed as an array.') + value = kwargs['choices'][0] + return coredata.UserComboOption(description, choices, value, yield_) + + @typed_kwargs( + 'integer option', + KwargInfo( + 'value', + (int, str), + default=True, + convertor=int, + ), + KwargInfo('min', (int, NoneType)), + KwargInfo('max', (int, NoneType)), + ) + def integer_parser(self, description: str, yield_: bool, kwargs: IntegerArgs) -> coredata.UserOption: + value = kwargs['value'] + inttuple = (kwargs['min'], kwargs['max'], value) + return coredata.UserIntegerOption(description, inttuple, yield_) + + @typed_kwargs( + 'string array option', + KwargInfo('value', (ContainerTypeInfo(list, str), str, NoneType)), + KwargInfo('choices', ContainerTypeInfo(list, str), default=[]), + ) + def string_array_parser(self, description: str, yield_: bool, kwargs: StringArrayArgs) -> coredata.UserOption: + choices = kwargs['choices'] + value = kwargs['value'] if kwargs['value'] is not None else choices return coredata.UserArrayOption(description, value, choices=choices, - yielding=kwargs['yield']) + yielding=yield_) - @permittedKwargs({'value', 'yield'}) - def feature_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - value = kwargs.get('value', 'auto') - return coredata.UserFeatureOption(description, value, kwargs['yield']) + @typed_kwargs( + 'feature option', + KwargInfo('value', str, default='auto', validator=in_set_validator({'auto', 'enabled', 'disabled'})), + ) + def feature_parser(self, description: str, yield_: bool, kwargs: FeatureArgs) -> coredata.UserOption: + return coredata.UserFeatureOption(description, kwargs['value'], yield_) diff --git a/test cases/failing/59 bad option argument/test.json b/test cases/failing/59 bad option argument/test.json index 3c5df1b27..28aa0caba 100644 --- a/test cases/failing/59 bad option argument/test.json +++ b/test cases/failing/59 bad option argument/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/59 bad option argument/meson_options.txt:1:0: ERROR: option got unknown keyword arguments \"vaule\"" + "line": "test cases/failing/59 bad option argument/meson_options.txt:1:0: ERROR: string option got unknown keyword arguments \"vaule\"" } ] }