From 8881a1941ab3bd11c84c5e029425ddc259c9bbfc Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 30 Aug 2024 11:12:57 -0700 Subject: [PATCH] options: split UserIntegerOption and UserUmaskOption They are very similar, but they are not exactly the same. By splitting them we can get full type safety, and run mypy over the options.py file! --- mesonbuild/mintro.py | 4 ++-- mesonbuild/options.py | 45 ++++++++++++++++++++++++++++--------------- run_mypy.py | 1 + 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index 404b247cd9fc..b16c2f41e2f6 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -315,7 +315,7 @@ def add_keys(opts: 'T.Union[dict[OptionKey, UserOption[Any]], cdata.KeyedOptionD elif isinstance(opt, options.UserComboOption): optdict['choices'] = opt.printable_choices() typestr = 'combo' - elif isinstance(opt, options.UserIntegerOption): + elif isinstance(opt, (options.UserIntegerOption, options.UserUmaskOption)): typestr = 'integer' elif isinstance(opt, options.UserStringArrayOption): typestr = 'array' @@ -323,7 +323,7 @@ def add_keys(opts: 'T.Union[dict[OptionKey, UserOption[Any]], cdata.KeyedOptionD if c: optdict['choices'] = c else: - raise RuntimeError("Unknown option type") + raise RuntimeError("Unknown option type: ", type(opt)) optdict['type'] = typestr optdict['description'] = opt.description optlist.append(optdict) diff --git a/mesonbuild/options.py b/mesonbuild/options.py index ae923fec2f63..48c2569ab9b1 100644 --- a/mesonbuild/options.py +++ b/mesonbuild/options.py @@ -29,7 +29,7 @@ from . import mlog if T.TYPE_CHECKING: - from typing_extensions import TypeAlias, TypedDict + from typing_extensions import Literal, TypeAlias, TypedDict DeprecatedType: TypeAlias = T.Union[bool, str, T.Dict[str, str], T.List[str]] @@ -319,13 +319,16 @@ def validate_value(self, value: T.Any) -> bool: return False raise MesonException(f'Option "{self.name}" value {value} is not boolean (true or false).') -@dataclasses.dataclass -class UserIntegerOption(UserOption[int]): - min_value: T.Optional[int] = None - max_value: T.Optional[int] = None +class _UserIntegerBase(UserOption[_T]): + + min_value: T.Optional[int] + max_value: T.Optional[int] + + if T.TYPE_CHECKING: + def toint(self, v: str) -> int: ... - def __post_init__(self, value_: int) -> None: + def __post_init__(self, value_: _T) -> None: super().__post_init__(value_) choices: T.List[str] = [] if self.min_value is not None: @@ -337,16 +340,23 @@ def __post_init__(self, value_: int) -> None: def printable_choices(self) -> T.Optional[T.List[str]]: return [self.__choices] - def validate_value(self, value: T.Any) -> int: + def validate_value(self, value: T.Any) -> _T: if isinstance(value, str): - value = self.toint(value) + value = T.cast('_T', self.toint(value)) if not isinstance(value, int): raise MesonException(f'Value {value!r} for option "{self.name}" is not an integer.') if self.min_value is not None and value < self.min_value: raise MesonException(f'Value {value} for option "{self.name}" is less than minimum value {self.min_value}.') if self.max_value is not None and value > self.max_value: raise MesonException(f'Value {value} for option "{self.name}" is more than maximum value {self.max_value}.') - return value + return T.cast('_T', value) + + +@dataclasses.dataclass +class UserIntegerOption(_UserIntegerBase[int]): + + min_value: T.Optional[int] = None + max_value: T.Optional[int] = None def toint(self, valuestring: str) -> int: try: @@ -354,6 +364,7 @@ def toint(self, valuestring: str) -> int: except ValueError: raise MesonException(f'Value string "{valuestring}" for option "{self.name}" is not convertible to an integer.') + class OctalInt(int): # NinjaBackend.get_user_option_args uses str() to converts it to a command line option # UserUmaskOption.toint() uses int(str, 8) to convert it to an integer @@ -361,28 +372,30 @@ class OctalInt(int): def __str__(self) -> str: return oct(int(self)) + @dataclasses.dataclass -class UserUmaskOption(UserIntegerOption, UserOption[T.Union[str, OctalInt]]): +class UserUmaskOption(_UserIntegerBase[T.Union["Literal['preserve']", OctalInt]]): min_value: T.Optional[int] = dataclasses.field(default=0, init=False) max_value: T.Optional[int] = dataclasses.field(default=0o777, init=False) def printable_value(self) -> str: - if self.value == 'preserve': - return self.value - return format(self.value, '04o') + if isinstance(self.value, int): + return format(self.value, '04o') + return self.value - def validate_value(self, value: T.Any) -> T.Union[str, OctalInt]: + def validate_value(self, value: T.Any) -> T.Union[Literal['preserve'], OctalInt]: if value == 'preserve': return 'preserve' return OctalInt(super().validate_value(value)) - def toint(self, valuestring: T.Union[str, OctalInt]) -> int: + def toint(self, valuestring: str) -> int: try: return int(valuestring, 8) except ValueError as e: raise MesonException(f'Invalid mode for option "{self.name}" {e}') + @dataclasses.dataclass class UserComboOption(EnumeratedUserOption[str]): @@ -580,7 +593,7 @@ def argparse_name_to_arg(name: str) -> str: return '--' + name.replace('_', '-') def prefixed_default(self, name: 'OptionKey', prefix: str = '') -> T.Any: - if self.opt_type in [UserComboOption, UserIntegerOption]: + if self.opt_type in {UserComboOption, UserIntegerOption, UserUmaskOption}: return self.default try: return BUILTIN_DIR_NOPREFIX_OPTIONS[name][prefix] diff --git a/run_mypy.py b/run_mypy.py index f72e96b3d3a2..18b154835993 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -75,6 +75,7 @@ 'mesonbuild/msetup.py', 'mesonbuild/mtest.py', 'mesonbuild/optinterpreter.py', + 'mesonbuild/options.py', 'mesonbuild/programs.py', ] additional = [