Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Option.default typing and definition checking #2173

Closed
wants to merge 10 commits into from
25 changes: 16 additions & 9 deletions Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ def __new__(mcs, name, bases, attrs):
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("option_")}

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 new_options, "Choice option 'random' cannot be manually assigned."
assert len(new_options) == len(set(new_options.values())), "same ID cannot be used twice. Try alias?"

Expand Down Expand Up @@ -96,7 +101,8 @@ def meta__init__(self, *args, **kwargs):

class Option(typing.Generic[T], metaclass=AssembleOptions):
value: T
default = 0
default: typing.ClassVar[typing.Union[T, typing.Literal["random"]]] # type: ignore
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore

# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
# Handled in get_option_name()
Expand Down Expand Up @@ -159,6 +165,7 @@ def verify(self, *args, **kwargs) -> None:
class FreeText(Option[str]):
"""Text option that allows users to enter strings.
Needs to be validated by the world or option definition."""
default: typing.ClassVar[typing.Union[str, typing.Literal["random"]]] = ""

def __init__(self, value: str):
assert isinstance(value, str), "value of FreeText must be a string"
Expand All @@ -182,7 +189,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
# (even though isinstance(5, numbers.Integral) == True)
Expand Down Expand Up @@ -353,7 +360,7 @@ def __xor__(self, other: typing.Any) -> int:
class Toggle(NumericOption):
option_false = 0
option_true = 1
default = 0
default: typing.ClassVar[typing.Union[int, typing.Literal["random"]]] = 0

def __init__(self, value: int):
assert value == 0 or value == 1, "value of Toggle can only be 0 or 1"
Expand Down Expand Up @@ -383,7 +390,7 @@ def get_option_name(cls, value):


class DefaultOnToggle(Toggle):
default = 1
default: typing.ClassVar[typing.Union[int, typing.Literal["random"]]] = 1


class Choice(NumericOption):
Expand Down Expand Up @@ -801,8 +808,8 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")


class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
default: typing.Dict[str, typing.Any] = {}
class OptionDict(Option[typing.Mapping[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
default: typing.ClassVar[typing.Union[typing.Mapping[str, typing.Any], typing.Literal["random"]]] = {}
supports_weighting = False

def __init__(self, value: typing.Dict[str, typing.Any]):
Expand Down Expand Up @@ -843,7 +850,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.List[typing.Any] = []
default: typing.ClassVar[typing.Union[typing.List[typing.Any], typing.Literal["random"]]] = []
supports_weighting = False

def __init__(self, value: typing.List[typing.Any]):
Expand All @@ -869,7 +876,7 @@ def __contains__(self, item):


class OptionSet(Option[typing.Set[str]], VerifyKeys):
default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset()
default: typing.ClassVar[typing.Union[typing.Set[str], typing.Literal["random"]]] = frozenset() # type: ignore
supports_weighting = False

def __init__(self, value: typing.Iterable[str]):
Expand Down Expand Up @@ -909,7 +916,7 @@ class Accessibility(Choice):
option_items = 1
option_minimal = 2
alias_none = 2
default = 1
default: typing.ClassVar[typing.Union[int, typing.Literal["random"]]] = 1


class ProgressionBalancing(NamedRange):
Expand Down
5 changes: 2 additions & 3 deletions worlds/soe/options.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass, fields
from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol
from typing import Any, cast, Dict, Iterator, List, Literal, Tuple, Protocol, Union

from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \
ProgressionBalancing, Range, Toggle
Expand All @@ -8,13 +8,12 @@
# typing boilerplate
class FlagsProtocol(Protocol):
value: int
default: int
flags: List[str]


class FlagProtocol(Protocol):
value: int
default: int
default: Union[int, Literal["random"]]
flag: str


Expand Down
Loading