Skip to content

Commit

Permalink
options: split UserIntegerOption and UserUmaskOption
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
dcbaker committed Aug 30, 2024
1 parent 0088e1d commit 2fbff61
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 18 deletions.
4 changes: 2 additions & 2 deletions mesonbuild/mintro.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,15 +315,15 @@ 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'
c = opt.printable_choices()
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)
Expand Down
45 changes: 29 additions & 16 deletions mesonbuild/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]

Expand Down Expand Up @@ -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:
Expand All @@ -337,52 +340,62 @@ 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:
return int(valuestring)
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
# So we need to use oct instead of dec here if we do not want values to be misinterpreted.
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]):

Expand Down Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions run_mypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
'mesonbuild/msetup.py',
'mesonbuild/mtest.py',
'mesonbuild/optinterpreter.py',
'mesonbuild/options.py',
'mesonbuild/programs.py',
]
additional = [
Expand Down

0 comments on commit 2fbff61

Please sign in to comment.