use typed_kwargs for the various option subparsers

We make use of allow_unknown=True here, which allows us to only look at
the common arguments in the main option parser, and then look at the
specific options in the dispatched parsers. This allows us to do more
specific checking on a per overload basis.
pull/11277/head
Dylan Baker 2 years ago committed by Eli Schwartz
parent eaf365cb3e
commit f5eaebb4b4
  1. 195
      mesonbuild/optinterpreter.py
  2. 2
      test cases/failing/59 bad option argument/test.json

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from __future__ import annotations
import re import re
import typing as T import typing as T
@ -19,11 +20,13 @@ from . import coredata
from . import mesonlib from . import mesonlib
from . import mparser from . import mparser
from . import mlog 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: if T.TYPE_CHECKING:
from .interpreterbase import TYPE_var, TYPE_kwargs from .interpreterbase import TYPE_var, TYPE_kwargs
from .interpreterbase import SubProject from .interpreterbase import SubProject
from typing_extensions import TypedDict from typing_extensions import TypedDict, Literal
FuncOptionArgs = TypedDict('FuncOptionArgs', { FuncOptionArgs = TypedDict('FuncOptionArgs', {
'type': str, 'type': str,
'description': str, 'description': str,
@ -34,13 +37,29 @@ if T.TYPE_CHECKING:
'max': T.Optional[int], 'max': T.Optional[int],
'deprecated': T.Union[bool, str, T.Dict[str, str], T.List[str]], 'deprecated': T.Union[bool, str, T.Dict[str, str], T.List[str]],
}) })
ParserArgs = TypedDict('ParserArgs', {
'yield': bool, class StringArgs(TypedDict):
'choices': T.Optional[T.List[str]], value: str
'value': object,
'min': T.Optional[int], class BooleanArgs(TypedDict):
'max': T.Optional[int], 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): class OptionException(mesonlib.MesonException):
@ -54,13 +73,14 @@ class OptionInterpreter:
def __init__(self, subproject: 'SubProject') -> None: def __init__(self, subproject: 'SubProject') -> None:
self.options: 'coredata.MutableKeyedOptionDictType' = {} self.options: 'coredata.MutableKeyedOptionDictType' = {}
self.subproject = subproject self.subproject = subproject
self.option_types = {'string': self.string_parser, self.option_types: T.Dict[str, T.Callable[..., coredata.UserOption]] = {
'boolean': self.boolean_parser, 'string': self.string_parser,
'combo': self.combo_parser, 'boolean': self.boolean_parser,
'integer': self.integer_parser, 'combo': self.combo_parser,
'array': self.string_array_parser, 'integer': self.integer_parser,
'feature': self.feature_parser, 'array': self.string_array_parser,
} 'feature': self.feature_parser,
}
def process(self, option_file: str) -> None: def process(self, option_file: str) -> None:
try: try:
@ -145,17 +165,25 @@ class OptionInterpreter:
(posargs, kwargs) = self.reduce_arguments(node.args) (posargs, kwargs) = self.reduce_arguments(node.args)
self.func_option(posargs, kwargs) self.func_option(posargs, kwargs)
@typed_kwargs('option', @typed_kwargs(
KwargInfo('type', str, required=True), 'option',
KwargInfo('description', str, default=''), KwargInfo(
KwargInfo('yield', bool, default=coredata.DEFAULT_YIELDING, since='0.45.0'), 'type',
KwargInfo('choices', (ContainerTypeInfo(list, str), type(None))), str,
KwargInfo('value', object), required=True,
KwargInfo('min', (int, type(None))), validator=in_set_validator({'string', 'boolean', 'integer', 'combo', 'array', 'feature'})
KwargInfo('max', (int, type(None))), ),
KwargInfo('deprecated', (bool, str, ContainerTypeInfo(dict, str), ContainerTypeInfo(list, str)), KwargInfo('description', str, default=''),
default=False, since='0.60.0') 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) @typed_pos_args('option', str)
def func_option(self, args: T.Tuple[str], kwargs: 'FuncOptionArgs') -> None: def func_option(self, args: T.Tuple[str], kwargs: 'FuncOptionArgs') -> None:
opt_name = args[0] opt_name = args[0]
@ -166,60 +194,81 @@ class OptionInterpreter:
raise OptionException('Option name %s is reserved.' % opt_name) raise OptionException('Option name %s is reserved.' % opt_name)
opt_type = kwargs['type'] opt_type = kwargs['type']
parser = self.option_types.get(opt_type) parser = self.option_types[opt_type]
if not parser:
raise OptionException(f'Unknown type {opt_type}.')
description = kwargs['description'] or opt_name description = kwargs['description'] or opt_name
# Only keep in kwargs arguments that are used by option type's parser # Drop the arguments we've already consumed
# because they use @permittedKwargs(). n_kwargs = {k: v for k, v in kwargs.items()
known_parser_kwargs = {'value', 'choices', 'yield', 'min', 'max'} if k not in {'type', 'description', 'deprecated', 'yield'}}
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)) opt = parser(description, kwargs['yield'], n_kwargs)
opt.deprecated = kwargs['deprecated'] 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: if key in self.options:
mlog.deprecation(f'Option {opt_name} already exists.') mlog.deprecation(f'Option {opt_name} already exists.')
self.options[key] = opt self.options[key] = opt
@permittedKwargs({'value', 'yield'}) @typed_kwargs(
def string_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: 'string option',
value = kwargs.get('value', '') KwargInfo('value', str, default=''),
return coredata.UserStringOption(description, value, kwargs['yield']) )
def string_parser(self, description: str, yield_: bool, kwargs: StringArgs) -> coredata.UserOption:
@permittedKwargs({'value', 'yield'}) return coredata.UserStringOption(description, kwargs['value'], yield_)
def boolean_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption:
value = kwargs.get('value', True) @typed_kwargs(
return coredata.UserBooleanOption(description, value, kwargs['yield']) 'boolean option',
KwargInfo(
@permittedKwargs({'value', 'yield', 'choices'}) 'value',
def combo_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: (bool, str),
choices = kwargs.get('choices') default=True,
if not choices: validator=lambda x: None if isinstance(x, bool) or x in {'true', 'false'} else 'boolean options must have boolean values',
raise OptionException('Combo option missing "choices" keyword.') ),
value = kwargs.get('value', choices[0]) )
return coredata.UserComboOption(description, choices, value, kwargs['yield']) def boolean_parser(self, description: str, yield_: bool, kwargs: BooleanArgs) -> coredata.UserOption:
return coredata.UserBooleanOption(description, kwargs['value'], yield_)
@permittedKwargs({'value', 'min', 'max', 'yield'})
def integer_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: @typed_kwargs(
value = kwargs.get('value') '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: if value is None:
raise OptionException('Integer option must contain value argument.') value = kwargs['choices'][0]
inttuple = (kwargs.get('min'), kwargs.get('max'), value) return coredata.UserComboOption(description, choices, value, yield_)
return coredata.UserIntegerOption(description, inttuple, kwargs['yield'])
@typed_kwargs(
@permittedKwargs({'value', 'yield', 'choices'}) 'integer option',
def string_array_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: KwargInfo(
choices = kwargs.get('choices', []) 'value',
value = kwargs.get('value', choices) (int, str),
if not isinstance(value, list): default=True,
raise OptionException('Array choices must be passed as an array.') 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, return coredata.UserArrayOption(description, value,
choices=choices, choices=choices,
yielding=kwargs['yield']) yielding=yield_)
@permittedKwargs({'value', 'yield'}) @typed_kwargs(
def feature_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: 'feature option',
value = kwargs.get('value', 'auto') KwargInfo('value', str, default='auto', validator=in_set_validator({'auto', 'enabled', 'disabled'})),
return coredata.UserFeatureOption(description, value, kwargs['yield']) )
def feature_parser(self, description: str, yield_: bool, kwargs: FeatureArgs) -> coredata.UserOption:
return coredata.UserFeatureOption(description, kwargs['value'], yield_)

@ -1,7 +1,7 @@
{ {
"stdout": [ "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\""
} }
] ]
} }

Loading…
Cancel
Save