From 2006da771c8a06e23f3fd4a6eecf67e54f3ce2cb Mon Sep 17 00:00:00 2001 From: beauxq Date: Mon, 4 Mar 2024 16:14:32 -0800 Subject: [PATCH] Core: typing for `Option.default` and a few other `Option` class variables This is a replacement for https://github.com/ArchipelagoMW/Archipelago/pull/2173 You can read discussion there for issues we found for why we can't have more specific typing on `default` instead of setting a default in `Option` (where we don't know the type), we check in the metaclass to make sure they have a default. --- Options.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Options.py b/Options.py index 139dc0a0bbe6..c0761b556cb6 100644 --- a/Options.py +++ b/Options.py @@ -41,6 +41,11 @@ def __new__(mcs, name, bases, attrs): aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if name.startswith("alias_")} + assert ( + name in {"Option", "VerifyKeys"} or # base abstract classes don't need default + "default" in attrs or + any(hasattr(base, "default") for base in bases) + ), f"Option class {name} needs default value" assert "random" not in aliases, "Choice option 'random' cannot be manually assigned." # auto-alias Off and On being parsed as True and False @@ -96,7 +101,7 @@ def meta__init__(self, *args, **kwargs): class Option(typing.Generic[T], metaclass=AssembleOptions): value: T - default = 0 + default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type # convert option_name_long into Name Long as display_name, otherwise name_long is the result. # Handled in get_option_name() @@ -106,8 +111,9 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): supports_weighting = True # filled by AssembleOptions: - name_lookup: typing.Dict[T, str] - options: typing.Dict[str, int] + name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore + # https://github.com/python/typing/discussions/1460 the reason for this type: ignore + options: typing.ClassVar[typing.Dict[str, int]] def __repr__(self) -> str: return f"{self.__class__.__name__}({self.current_option_name})" @@ -160,6 +166,8 @@ class FreeText(Option[str]): """Text option that allows users to enter strings. Needs to be validated by the world or option definition.""" + default = "" + def __init__(self, value: str): assert isinstance(value, str), "value of FreeText must be a string" self.value = value @@ -182,7 +190,7 @@ def get_option_name(cls, value: str) -> str: class NumericOption(Option[int], numbers.Integral, abc.ABC): - default = 0 + default: typing.ClassVar[typing.Union[int, typing.Literal["random"]]] = 0 # note: some of the `typing.Any`` here is a result of unresolved issue in python standards # `int` is not a `numbers.Integral` according to the official typestubs @@ -803,7 +811,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]): - default: typing.Dict[str, typing.Any] = {} + default = {} supports_weighting = False def __init__(self, value: typing.Dict[str, typing.Any]): @@ -844,7 +852,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys): # If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead. # Not a docstring so it doesn't get grabbed by the options system. - default: typing.Union[typing.List[typing.Any], typing.Tuple[typing.Any, ...]] = () + default = () supports_weighting = False def __init__(self, value: typing.Iterable[str]): @@ -870,7 +878,7 @@ def __contains__(self, item): class OptionSet(Option[typing.Set[str]], VerifyKeys): - default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset() + default = frozenset() supports_weighting = False def __init__(self, value: typing.Iterable[str]):