Core: typing for `Option.default` and a few other ClassVars (#2899)

* 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.

* NumericOption doesn't need the type annotation that brings out the mypy bug

* SoE default ClassVar
This commit is contained in:
Doug Hoskisson 2024-03-12 14:03:57 -07:00 committed by GitHub
parent f8d5fe0e1e
commit 03d403ff51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 17 additions and 9 deletions

View File

@ -41,6 +41,11 @@ class AssembleOptions(abc.ABCMeta):
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("alias_")} 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." assert "random" not in aliases, "Choice option 'random' cannot be manually assigned."
# auto-alias Off and On being parsed as True and False # auto-alias Off and On being parsed as True and False
@ -96,7 +101,7 @@ T = typing.TypeVar('T')
class Option(typing.Generic[T], metaclass=AssembleOptions): class Option(typing.Generic[T], metaclass=AssembleOptions):
value: T 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. # convert option_name_long into Name Long as display_name, otherwise name_long is the result.
# Handled in get_option_name() # Handled in get_option_name()
@ -106,8 +111,9 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
supports_weighting = True supports_weighting = True
# filled by AssembleOptions: # filled by AssembleOptions:
name_lookup: typing.Dict[T, str] name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore
options: typing.Dict[str, int] # https://github.com/python/typing/discussions/1460 the reason for this type: ignore
options: typing.ClassVar[typing.Dict[str, int]]
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.current_option_name})" 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. """Text option that allows users to enter strings.
Needs to be validated by the world or option definition.""" Needs to be validated by the world or option definition."""
default = ""
def __init__(self, value: str): def __init__(self, value: str):
assert isinstance(value, str), "value of FreeText must be a string" assert isinstance(value, str), "value of FreeText must be a string"
self.value = value self.value = value
@ -811,7 +819,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]): class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
default: typing.Dict[str, typing.Any] = {} default = {}
supports_weighting = False supports_weighting = False
def __init__(self, value: typing.Dict[str, typing.Any]): def __init__(self, value: typing.Dict[str, typing.Any]):
@ -852,7 +860,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. # 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. # 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 supports_weighting = False
def __init__(self, value: typing.Iterable[typing.Any]): def __init__(self, value: typing.Iterable[typing.Any]):
@ -878,7 +886,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
class OptionSet(Option[typing.Set[str]], VerifyKeys): class OptionSet(Option[typing.Set[str]], VerifyKeys):
default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset() default = frozenset()
supports_weighting = False supports_weighting = False
def __init__(self, value: typing.Iterable[str]): def __init__(self, value: typing.Iterable[str]):

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass, fields from dataclasses import dataclass, fields
from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol from typing import Any, ClassVar, cast, Dict, Iterator, List, Tuple, Protocol
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \ from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \
ProgressionBalancing, Range, Toggle ProgressionBalancing, Range, Toggle
@ -8,13 +8,13 @@ from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option,
# typing boilerplate # typing boilerplate
class FlagsProtocol(Protocol): class FlagsProtocol(Protocol):
value: int value: int
default: int default: ClassVar[int]
flags: List[str] flags: List[str]
class FlagProtocol(Protocol): class FlagProtocol(Protocol):
value: int value: int
default: int default: ClassVar[int]
flag: str flag: str