Options: Add more Option unit tests, add generic Option.__eq__, make cull_zeroes available to all OptionCounters #5905

This commit is contained in:
NewSoupVi
2026-04-18 12:49:19 +01:00
committed by GitHub
parent 66712bbd87
commit 0a742b6c98
2 changed files with 128 additions and 9 deletions

View File

@@ -212,6 +212,13 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
else:
return cls.name_lookup[value]
def __eq__(self, other: typing.Any) -> bool:
if isinstance(other, self.__class__):
return self.value == other.value
if isinstance(other, Option):
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
return self.value == other
def __int__(self) -> T:
return self.value
@@ -930,13 +937,34 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
class OptionCounter(OptionDict):
min: int | None = None
max: int | None = None
cull_zeroes: bool = False
def __init__(self, value: dict[str, int]) -> None:
super(OptionCounter, self).__init__(collections.Counter(value))
cleaned_dict = {}
invalid_value_errors = []
for key, value in value.items():
if not isinstance(value, (int, float)) or int(value) != value:
invalid_value_errors += [f"Invalid value {value} for key {key}, must be an integer."]
continue
if self.cull_zeroes and value == 0:
continue
cleaned_dict[key] = int(value)
if invalid_value_errors:
type_errors = [f"For option {self.__class__.__name__}:"] + invalid_value_errors
raise TypeError("\n".join(invalid_value_errors))
super(OptionCounter, self).__init__(collections.Counter(cleaned_dict))
def verify(self, world: type[World], player_name: str, plando_options: PlandoOptions) -> None:
super(OptionCounter, self).verify(world, player_name, plando_options)
self.verify_values()
def verify_values(self):
range_errors = []
if self.max is not None:
@@ -959,13 +987,8 @@ class OptionCounter(OptionDict):
class ItemDict(OptionCounter):
verify_item_name = True
min = 0
def __init__(self, value: dict[str, int]) -> None:
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
value = {item_name: amount for item_name, amount in value.items() if amount != 0}
super(ItemDict, self).__init__(value)
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
cull_zeroes = True
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):

View File

@@ -1,6 +1,8 @@
import unittest
from Options import Choice, DefaultOnToggle, Toggle
from collections import Counter
from Options import Choice, DefaultOnToggle, Toggle, OptionDict, OptionError, OptionSet, OptionList, OptionCounter
class TestNumericOptions(unittest.TestCase):
@@ -74,3 +76,97 @@ class TestNumericOptions(unittest.TestCase):
self.assertTrue(toggle_string)
self.assertTrue(toggle_int)
self.assertTrue(toggle_alias)
class TestContainerOptions(unittest.TestCase):
def test_option_dict(self):
class TestOptionDict(OptionDict):
valid_keys = frozenset({"A", "B", "C"})
unknown_key_init_dict = {"D": "Foo"}
test_option_dict = TestOptionDict(unknown_key_init_dict)
self.assertRaises(OptionError, test_option_dict.verify_keys)
init_dict = {"A": "foo", "B": "bar"}
test_option_dict = TestOptionDict(init_dict)
self.assertEqual(test_option_dict, init_dict) # Implicit value comparison
self.assertEqual(test_option_dict["A"], "foo")
self.assertIn("B", test_option_dict)
self.assertNotIn("C", test_option_dict)
self.assertRaises(KeyError, lambda: test_option_dict["C"])
def test_option_set(self):
class TestOptionSet(OptionSet):
valid_keys = frozenset({"A", "B", "C"})
unknown_key_init_set = {"D"}
test_option_set = TestOptionSet(unknown_key_init_set)
self.assertRaises(OptionError, test_option_set.verify_keys)
init_set = {"A", "B"}
test_option_set = TestOptionSet(init_set)
self.assertEqual(test_option_set, init_set) # Implicit value comparison
self.assertIn("B", test_option_set)
self.assertNotIn("C", test_option_set)
def test_option_list(self):
class TestOptionList(OptionList):
valid_keys = frozenset({"A", "B", "C"})
unknown_key_init_list = ["D"]
test_option_list = TestOptionList(unknown_key_init_list)
self.assertRaises(OptionError, test_option_list.verify_keys)
init_list = ["A", "B"]
test_option_list = TestOptionList(init_list)
self.assertEqual(test_option_list, init_list)
self.assertIn("B", test_option_list)
self.assertNotIn("C", test_option_list)
def test_option_counter(self):
class TestOptionCounter(OptionCounter):
valid_keys = frozenset({"A", "B", "C"})
max = 10
min = 0
unknown_key_init_dict = {"D": 5}
test_option_counter = TestOptionCounter(unknown_key_init_dict)
self.assertRaises(OptionError, test_option_counter.verify_keys)
wrong_value_type_init_dict = {"A": "B"}
self.assertRaises(TypeError, TestOptionCounter, wrong_value_type_init_dict)
violates_max_init_dict = {"A": 5, "B": 11}
test_option_counter = TestOptionCounter(violates_max_init_dict)
self.assertRaises(OptionError, test_option_counter.verify_values)
violates_min_init_dict = {"A": -1, "B": 5}
test_option_counter = TestOptionCounter(violates_min_init_dict)
self.assertRaises(OptionError, test_option_counter.verify_values)
init_dict = {"A": 0, "B": 10}
test_option_counter = TestOptionCounter(init_dict)
self.assertEqual(test_option_counter, Counter(init_dict))
self.assertIn("A", test_option_counter)
self.assertNotIn("C", test_option_counter)
self.assertEqual(test_option_counter["A"], 0)
self.assertEqual(test_option_counter["B"], 10)
self.assertEqual(test_option_counter["C"], 0)
def test_culling_option_counter(self):
class TestCullingCounter(OptionCounter):
valid_keys = frozenset({"A", "B", "C"})
cull_zeroes = True
init_dict = {"A": 0, "B": 10}
test_option_counter = TestCullingCounter(init_dict)
self.assertNotIn("A", test_option_counter)
self.assertIn("B", test_option_counter)
self.assertNotIn("C", test_option_counter)
self.assertEqual(test_option_counter["A"], 0) # It's still a Counter! cull_zeroes is about "in" checks.
self.assertEqual(test_option_counter, Counter({"B": 10}))