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
# 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_)

@ -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\""
}
]
}

Loading…
Cancel
Save