Options: Always verify keys for VerifyKeys options (#3280)

* Options: Always verify keys for VerifyKeys options

* fix PlandoTexts

* use OptionError and give a slightly better error message for which option it is

* add the player name to the error

* don't create an unnecessary list

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
Aaron Wagener 2024-07-31 10:37:52 -05:00 committed by GitHub
parent 91f7cf16de
commit 53bc4ffa52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 28 additions and 14 deletions

View File

@ -786,17 +786,22 @@ class VerifyKeys(metaclass=FreezeValidKeys):
verify_location_name: bool = False verify_location_name: bool = False
value: typing.Any value: typing.Any
@classmethod def verify_keys(self) -> None:
def verify_keys(cls, data: typing.Iterable[str]) -> None: if self.valid_keys:
if cls.valid_keys: data = set(self.value)
data = set(data) dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data)
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) extra = dataset - self._valid_keys
extra = dataset - cls._valid_keys
if extra: if extra:
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " raise OptionError(
f"Allowed keys: {cls._valid_keys}.") f"Found unexpected key {', '.join(extra)} in {getattr(self, 'display_name', self)}. "
f"Allowed keys: {self._valid_keys}."
)
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
try:
self.verify_keys()
except OptionError as validation_error:
raise OptionError(f"Player {player_name} has invalid option keys:\n{validation_error}")
if self.convert_name_groups and self.verify_item_name: if self.convert_name_groups and self.verify_item_name:
new_value = type(self.value)() # empty container of whatever value is new_value = type(self.value)() # empty container of whatever value is
for item_name in self.value: for item_name in self.value:
@ -833,7 +838,6 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
@classmethod @classmethod
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict: def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
if type(data) == dict: if type(data) == dict:
cls.verify_keys(data)
return cls(data) return cls(data)
else: else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
@ -879,7 +883,6 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
@classmethod @classmethod
def from_any(cls, data: typing.Any): def from_any(cls, data: typing.Any):
if is_iterable_except_str(data): if is_iterable_except_str(data):
cls.verify_keys(data)
return cls(data) return cls(data)
return cls.from_text(str(data)) return cls.from_text(str(data))
@ -905,7 +908,6 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
@classmethod @classmethod
def from_any(cls, data: typing.Any): def from_any(cls, data: typing.Any):
if is_iterable_except_str(data): if is_iterable_except_str(data):
cls.verify_keys(data)
return cls(data) return cls(data)
return cls.from_text(str(data)) return cls.from_text(str(data))
@ -948,6 +950,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
self.value = [] self.value = []
logging.warning(f"The plando texts module is turned off, " logging.warning(f"The plando texts module is turned off, "
f"so text for {player_name} will be ignored.") f"so text for {player_name} will be ignored.")
else:
super().verify(world, player_name, plando_options)
def verify_keys(self) -> None:
if self.valid_keys:
data = set(text.at for text in self)
dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data)
extra = dataset - self._valid_keys
if extra:
raise OptionError(
f"Invalid \"at\" placement {', '.join(extra)} in {getattr(self, 'display_name', self)}. "
f"Allowed placements: {self._valid_keys}."
)
@classmethod @classmethod
def from_any(cls, data: PlandoTextsFromAnyType) -> Self: def from_any(cls, data: PlandoTextsFromAnyType) -> Self:
@ -971,7 +986,6 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
texts.append(text) texts.append(text)
else: else:
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
cls.verify_keys([text.at for text in texts])
return cls(texts) return cls(texts)
else: else:
raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}")

View File

@ -6,7 +6,7 @@ from argparse import Namespace
from contextlib import contextmanager from contextlib import contextmanager
from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any
from BaseClasses import MultiWorld, CollectionState, get_seed, Location, Item, ItemClassification from BaseClasses import MultiWorld, CollectionState, PlandoOptions, get_seed, Location, Item, ItemClassification
from Options import VerifyKeys from Options import VerifyKeys
from test.bases import WorldTestBase from test.bases import WorldTestBase
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
@ -365,7 +365,7 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp
if issubclass(option, VerifyKeys): if issubclass(option, VerifyKeys):
# Values should already be verified, but just in case... # Values should already be verified, but just in case...
option.verify_keys(value.value) value.verify(StardewValleyWorld, "Tester", PlandoOptions.bosses)
setattr(args, name, {1: value}) setattr(args, name, {1: value})
multiworld.set_options(args) multiworld.set_options(args)