From 0a742b6c984fc7ce03bb52b7a4a4b86b60831403 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:49:19 +0100 Subject: [PATCH] Options: Add more Option unit tests, add generic Option.__eq__, make cull_zeroes available to all OptionCounters #5905 --- Options.py | 39 +++++++++--- test/options/test_option_classes.py | 98 ++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/Options.py b/Options.py index 57119ff66c..a84d5e280e 100644 --- a/Options.py +++ b/Options.py @@ -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): diff --git a/test/options/test_option_classes.py b/test/options/test_option_classes.py index ca90db8870..8ca1751fa3 100644 --- a/test/options/test_option_classes.py +++ b/test/options/test_option_classes.py @@ -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}))