From 2cb0bf0f4fd675852b95122af7041510fc3efb06 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Sat, 23 Nov 2024 11:05:15 -0800 Subject: [PATCH] added enum fallback option to dataclassio --- .efrocachemap | 72 +++++----- CHANGELOG.md | 2 +- .../ba_data/python/baclassic/_appmode.py | 34 ++++- src/assets/ba_data/python/baenv.py | 2 +- src/assets/ba_data/python/baplus/_cloud.py | 10 +- src/ballistica/shared/ballistica.cc | 2 +- tests/test_efro/test_dataclassio.py | 124 +++++++++++++----- tools/bacommon/cloud.py | 9 ++ tools/efro/call.py | 19 +-- tools/efro/dataclassio/_base.py | 6 + tools/efro/dataclassio/_inputter.py | 11 +- tools/efro/dataclassio/_prep.py | 20 ++- tools/efro/logging.py | 2 +- 13 files changed, 219 insertions(+), 94 deletions(-) diff --git a/.efrocachemap b/.efrocachemap index ed8df9c9..58d7ae8a 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -4099,42 +4099,42 @@ "build/assets/windows/Win32/ucrtbased.dll": "bfd1180c269d3950b76f35a63655e9e1", "build/assets/windows/Win32/vc_redist.x86.exe": "15a5f1f876503885adbdf5b3989b3718", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", - "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "809ad854c769646c9bccf9f72bfd2f9f", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "84f339cbec284e824904613e59cab1a2", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "2c385ed88eaf7787c4bf36b2681ccd91", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "2156812371a87399fb3e0fff856fc739", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "40b5aacb91774c9343d674a15801f6d9", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "8833e259a48cdb79a958d0ed18b56a65", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "b5b8df8478e0d8513f6273d00bf97779", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "e016a4a952ee8947169a2dfc34718a9f", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "fffe409a157aa345ba9193f992d2b752", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "df686ee58c36af1100d20fa7a4deb08e", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "70373843b698d5c4400a9cbb916d8d21", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "f68567c5d2d5ea680c26578c2dc7a912", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "71137c9d5d96923d4d5687de021488cd", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "f37c138bc5b6c17effa49159cc30db91", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "fecc9bbd4ebb50b633313077206c1504", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "2d30efa541a0bd38fe578ce0baf929a6", - "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "e8b47cf217d05ce0df5a334e3b456ced", - "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "014014670516ce58a7d7dd3c39f12c84", - "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "e8b47cf217d05ce0df5a334e3b456ced", - "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "014014670516ce58a7d7dd3c39f12c84", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "713c6046e9e061ac045c65f593a1dafb", - "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "107aa28e8e7568ff6fca408728b80d94", - "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "713c6046e9e061ac045c65f593a1dafb", - "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "107aa28e8e7568ff6fca408728b80d94", - "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "ffe19b0ae94217a0ed7ccc3d46c97a24", - "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "3f3a18dc4d6118ec181d03df1d0bd4de", - "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "ffe19b0ae94217a0ed7ccc3d46c97a24", - "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "3f3a18dc4d6118ec181d03df1d0bd4de", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "6eeb79220b31acc56da748201090218f", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "ade915a77be68916691c0329f8cfdd9f", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "67b55ef9f0d683340ef61c1df08de181", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "689fd22835bb965b5e7fafe5149f8319", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "fdc1fa0dbaca6e517f2608bcc7476e96", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "6af6ad18c0486410454db2a53b72e458", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "f496932bed2c235654829404741b4526", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "5ec6c0c883ca79b2eec1f7b952ecaa39", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "0921e644948438c78c8621f1199dc6d6", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "54486e895c6be0128a2e25624aaef051", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "bcfc357760d1030c66e3b65c30a649c8", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "a398b7f179cf6ee31af8037bae59bdc4", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "9c5e37721e8834491c249b3f1e986e8a", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "57dd4fa7356ea5a1a102c5a0422a4485", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "cad5283f88be83233e7a8b674db02c17", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "f36a3b1ef5d5650c373ac3230f3e64a3", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "327abf1bd532b104cbb63fa9dd11f9b7", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "685e787af247ba2694c7632c469e4853", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "6c41aad3f4c60d56350be66a6ba28a3f", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "d752bc8ee063e3d95038b3ce8bdb0e3c", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "5f2e7c646ce28814d5d87ba2e4b826c4", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "b440e073afd70d018fc14ee10f41bb85", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "bc56615fbc3d671deebf36942b36c706", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "e5d47e94497951d438520cde86d487b2", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "86eaadb3dee0a1f1137192fe943996f6", + "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "8c795d1a871f0d82a198b5aeb506d770", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "86eaadb3dee0a1f1137192fe943996f6", + "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "8c795d1a871f0d82a198b5aeb506d770", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "ab0cf0e9d6001748927660d84c1da87f", + "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "62c22d7a25fd62831cb4a089bbee1b0b", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "ab0cf0e9d6001748927660d84c1da87f", + "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "62c22d7a25fd62831cb4a089bbee1b0b", + "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "75a5ab56d54304e602fff8dae6435904", + "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "19b6e806d066affc80b32c4899c6ba2f", + "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "75a5ab56d54304e602fff8dae6435904", + "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "19b6e806d066affc80b32c4899c6ba2f", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "ca6096df15041b6af9a89ed81880c98b", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "f06c730c9cee3599679df778ec565842", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "1a77179b3548e78791ddd1205ff505a3", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "d755dc558bba71715ed0b543fa966be4", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "722aff67a010cc0a435bc70018b0b49a", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "373deb2c02283978f415c1b937229292", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "fe26aafc763a1198e1921caeb261bdb8", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "73f3c9211175f452e15c45296db89744", "src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c", "src/assets/ba_data/python/babase/_mgen/enums.py": "794d258d59fd17a61752843a9a0551ad", "src/ballistica/base/mgen/pyembed/binding_base.inc": "3a583e7e03bd4907b21adc3bf5729d15", diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6b9399..4394d4ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.37 (build 22110, api 9, 2024-11-22) +### 1.7.37 (build 22112, api 9, 2024-11-23) - Bumping api version to 9. As you'll see below, there's some UI changes that will require a bit of work for any UI mods to adapt to. If your mods don't touch UI stuff at all you can simply bump your api version and call it a day. diff --git a/src/assets/ba_data/python/baclassic/_appmode.py b/src/assets/ba_data/python/baclassic/_appmode.py index 9a6bc780..e19942e3 100644 --- a/src/assets/ba_data/python/baclassic/_appmode.py +++ b/src/assets/ba_data/python/baclassic/_appmode.py @@ -25,6 +25,7 @@ if TYPE_CHECKING: from typing import Callable, Any from efro.call import CallbackRegistration + from bacommon.cloud import ClassicAccountData from babase import AppIntent, AccountV2Handle, CloudSubscription from bauiv1 import UIV1AppSubsystem, MainWindow, MainWindowState @@ -38,6 +39,7 @@ class ClassicAppMode(AppMode): CallbackRegistration | None ) = None self._test_sub: CloudSubscription | None = None + self._account_data_sub: CloudSubscription | None = None @override @classmethod @@ -122,7 +124,7 @@ class ClassicAppMode(AppMode): # We want to be informed when primary account changes. self._on_primary_account_changed_callback = ( - app.plus.accounts.on_primary_account_changed_callbacks.add( + app.plus.accounts.on_primary_account_changed_callbacks.register( self.update_for_primary_account ) ) @@ -135,7 +137,7 @@ class ClassicAppMode(AppMode): # Stop being informed of account changes. self._on_primary_account_changed_callback = None - # Remove any listeners for any current primary account. + # Remove anything following any current account. self.update_for_primary_account(None) # Save where we were in the UI so we return there next time. @@ -157,10 +159,10 @@ class ClassicAppMode(AppMode): ) -> None: """Update subscriptions/etc. for a new primary account state.""" assert in_logic_thread() + assert app.plus is not None # For testing subscription functionality. if os.environ.get('BA_SUBSCRIPTION_TEST') == '1': - assert app.plus is not None if account is None: self._test_sub = None else: @@ -171,9 +173,22 @@ class ClassicAppMode(AppMode): else: self._test_sub = None + if account is None or bool(True): + self._account_data_sub = None + else: + with account: + self._account_data_sub = ( + app.plus.cloud.subscribe_classic_account_data( + self._on_classic_account_data_change + ) + ) + def _on_sub_test_update(self, val: int | None) -> None: print(f'GOT SUB TEST UPDATE: {val}') + def _on_classic_account_data_change(self, val: ClassicAccountData) -> None: + print(f'GOT CLASSIC ACCOUNT DATA: {val}') + def _root_ui_menu_press(self) -> None: from babase import push_back_press @@ -184,7 +199,6 @@ class ClassicAppMode(AppMode): if old_window is not None: classic = app.classic - assert classic is not None classic.resume() @@ -230,7 +244,13 @@ class ClassicAppMode(AppMode): win_type: type[MainWindow], win_create_call: Callable[[], MainWindow], ) -> None: - """Navigate to or away from a particular type of Auxiliary window.""" + """Navigate to or away from an Auxiliary window. + + Auxiliary windows can be thought of as 'side quests' in the + window hierarchy; places such as settings windows or league + ranking windows that the user might want to visit without losing + their place in the regular hierarchy. + """ # pylint: disable=unidiomatic-typecheck ui = app.ui_v1 @@ -286,8 +306,8 @@ class ClassicAppMode(AppMode): ) return - # Ok, no auxiliary states found. Now if current window is auxiliary - # and the type matches, simply do a back. + # Ok, no auxiliary states found. Now if current window is + # auxiliary and the type matches, simply do a back. if ( current_main_window.main_window_is_auxiliary and type(current_main_window) is win_type diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index 48d2f009..9956e98a 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -53,7 +53,7 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 22110 +TARGET_BALLISTICA_BUILD = 22112 TARGET_BALLISTICA_VERSION = '1.7.37' diff --git a/src/assets/ba_data/python/baplus/_cloud.py b/src/assets/ba_data/python/baplus/_cloud.py index 9e77a1ba..12f6e02f 100644 --- a/src/assets/ba_data/python/baplus/_cloud.py +++ b/src/assets/ba_data/python/baplus/_cloud.py @@ -177,7 +177,15 @@ class CloudSubsystem(babase.AppSubsystem): def subscribe_test( self, updatecall: Callable[[int | None], None] ) -> babase.CloudSubscription: - """Subscribe to some data.""" + """Subscribe to some test data.""" + raise NotImplementedError( + 'Cloud functionality is not present in this build.' + ) + + def subscribe_classic_account_data( + self, updatecall: Callable[[bacommon.cloud.ClassicAccountData], None] + ) -> babase.CloudSubscription: + """Subscribe to classic account data.""" raise NotImplementedError( 'Cloud functionality is not present in this build.' ) diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index 976e7b83..2eeb9e06 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 22110; +const int kEngineBuildNumber = 22112; const char* kEngineVersion = "1.7.37"; const int kEngineApiVersion = 9; diff --git a/tests/test_efro/test_dataclassio.py b/tests/test_efro/test_dataclassio.py index 55d6e84b..a24e5617 100644 --- a/tests/test_efro/test_dataclassio.py +++ b/tests/test_efro/test_dataclassio.py @@ -588,7 +588,8 @@ def test_dict() -> None: obj = _TestClass(dval={}) - # 'Any' dicts should only support values directly compatible with json. + # 'Any' dicts should only support values directly compatible with + # json. obj.dval['foo'] = 5 dataclass_to_dict(obj) with pytest.raises(TypeError): @@ -598,8 +599,8 @@ def test_dict() -> None: obj.dval['foo'] = _GoodEnum.VAL1 dataclass_to_dict(obj) - # Int dict-keys should actually be stored as strings internally - # (for json compatibility). + # Int dict-keys should actually be stored as strings internally (for + # json compatibility). @ioprepped @dataclass class _TestClass2: @@ -753,8 +754,8 @@ def test_any() -> None: obj = _TestClass(anyval=b'bytes') - # JSON output doesn't allow bytes or datetime objects - # included in 'Any' data. + # JSON output doesn't allow bytes or datetime objects included in + # 'Any' data. with pytest.raises(TypeError): dataclass_validate(obj, codec=Codec.JSON) @@ -831,8 +832,8 @@ def test_datetime_limits() -> None: def test_field_paths() -> None: """Test type-safe field path evaluations.""" - # Define a few nested dataclass types, some of which - # have storage names differing from their field names. + # Define a few nested dataclass types, some of which have storage + # names differing from their field names. @ioprepped @dataclass class _TestClass: @@ -856,13 +857,13 @@ def test_field_paths() -> None: assert lookup.path(lambda obj: obj.sub2.val1) == 's2.val1' assert lookup.path(lambda obj: obj.sub2.val2) == 's2.v2' - # Attempting to return fields that aren't there should fail - # in both type-checking and runtime. + # Attempting to return fields that aren't there should fail in both + # type-checking and runtime. with pytest.raises(AttributeError): lookup.path(lambda obj: obj.sub1.val3) # type: ignore - # Returning non-field objects will fail at runtime - # even if type-checking evaluates them as valid values. + # Returning non-field objects will fail at runtime even if + # type-checking evaluates them as valid values. with pytest.raises(TypeError): lookup.path(lambda obj: 1) @@ -905,8 +906,8 @@ def test_extended_data() -> None: with pytest.raises(ValueError): _obj = dataclass_from_dict(_TestClass, indata) - # Now define the same data but give it an adapter - # so it can work with our incorrectly-formatted data. + # Now define the same data but give it an adapter so it can work + # with our incorrectly-formatted data. @ioprepped @dataclass class _TestClass2(IOExtendedData): @@ -978,8 +979,8 @@ def test_soft_default() -> None: with pytest.raises(ValueError): dataclass_from_dict(_TestClassA2, {}) - # These should succeed because it has a soft-default value to - # fall back on. + # These should succeed because it has a soft-default value to fall + # back on. dataclass_from_dict(_TestClassB, {}) dataclass_from_dict(_TestClassB2, {}) dataclass_from_dict(_TestClassB3, {}) @@ -1020,8 +1021,8 @@ def test_soft_default() -> None: assert dataclass_to_dict(_TestClassC3b(0)) == {} - # We disallow passing a few mutable types as soft_defaults - # just as dataclass does with regular defaults. + # We disallow passing a few mutable types as soft_defaults just as + # dataclass does with regular defaults. with pytest.raises(TypeError): @ioprepped @@ -1044,9 +1045,9 @@ def test_soft_default() -> None: class _TestClassD3: lval: Annotated[dict, IOAttrs(soft_default={})] - # soft_defaults are not static-type-checked, but we do try to - # catch basic type mismatches at prep time. Make sure that's working. - # (we also do full value validation during input, but the more we catch + # soft_defaults are not static-type-checked, but we do try to catch + # basic type mismatches at prep time. Make sure that's working. (we + # also do full value validation during input, but the more we catch # early the better) with pytest.raises(TypeError): @@ -1069,9 +1070,9 @@ def test_soft_default() -> None: class _TestClassE3: lval: Annotated[list, IOAttrs(soft_default_factory=set)] - # Make sure Unions/Optionals go through ok. - # (note that mismatches currently aren't caught at prep time; just - # checking the negative case here). + # Make sure Unions/Optionals go through ok. Note that mismatches + # currently aren't caught at prep time; just checking the negative + # case here. @ioprepped @dataclass class _TestClassE4: @@ -1083,7 +1084,8 @@ def test_soft_default() -> None: lval: Annotated[str | None, IOAttrs(soft_default='foo')] # Now try more in-depth examples: nested type mismatches like this - # are currently not caught at prep-time but ARE caught during inputting. + # are currently not caught at prep-time but ARE caught during + # inputting. @ioprepped @dataclass class _TestClassE6: @@ -1100,9 +1102,9 @@ def test_soft_default() -> None: with pytest.raises(TypeError): dataclass_from_dict(_TestClassE7, {}) - # If both a soft_default and regular field default are present, - # make sure soft_default takes precedence (it applies before - # data even hits the dataclass constructor). + # If both a soft_default and regular field default are present, make + # sure soft_default takes precedence (it applies before data even + # hits the dataclass constructor). @ioprepped @dataclass @@ -1111,8 +1113,8 @@ def test_soft_default() -> None: assert dataclass_from_dict(_TestClassE8, {}).ival == 1 - # Make sure soft_default gets used both when determining when - # to omit values from output and what to recreate missing values as. + # Make sure soft_default gets used both when determining when to + # omit values from output and what to recreate missing values as. orig = _TestClassE8(ival=1) todict = dataclass_to_dict(orig) assert todict == {} @@ -1168,11 +1170,10 @@ class MTTestBase(IOMultiType[MTTestTypeID]): @classmethod def get_type_id(cls) -> MTTestTypeID: """Provide the type-id for this subclass.""" - # If we wanted, we could just maintain a static mapping - # of types-to-ids here, but there are benefits to letting - # each child class speak for itself. Namely that we can - # do lazy-loading and don't need to have all types present - # here. + # If we wanted, we could just maintain a static mapping of + # types-to-ids here, but there are benefits to letting each + # child class speak for itself. Namely that we can do + # lazy-loading and don't need to have all types present here. # So we'll let all our child classes override this. raise NotImplementedError() @@ -1457,3 +1458,58 @@ def test_multi_type_2() -> None: indict3 = {'type': 'm3'} with pytest.raises(RuntimeError): val3 = dataclass_from_dict(MTTest2Base, indict3) + + +def test_enum_fallback() -> None: + """Test enum_fallback IOAttr values.""" + # pylint: disable=missing-class-docstring + # pylint: disable=unused-variable + + @ioprepped + @dataclass + class TestClass: + + class TestEnum1(Enum): + VAL1 = 'val1' + VAL2 = 'val2' + VAL3 = 'val3' + + class TestEnum2(Enum): + VAL1 = 'val1' + VAL2 = 'val2' + VAL3 = 'val3' + + enum1val: Annotated[TestEnum1, IOAttrs('e1')] + enum2val: Annotated[ + TestEnum2, IOAttrs('e2', enum_fallback=TestEnum2.VAL1) + ] + + # All valid values; should work. + _obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val1'}) + + # Bad Enum1 value; should fail since there's no fallback. + with pytest.raises(ValueError): + _obj = dataclass_from_dict(TestClass, {'e1': 'val4', 'e2': 'val1'}) + + # Bad Enum2 value; should substitute our fallback value. + obj = dataclass_from_dict(TestClass, {'e1': 'val1', 'e2': 'val4'}) + assert obj.enum2val is obj.TestEnum2.VAL1 + + # Using wrong type as enum_fallback should fail. + with pytest.raises(TypeError): + + @ioprepped + @dataclass + class TestClass2: + + class TestEnum1(Enum): + VAL1 = 'val1' + VAL2 = 'val2' + + class TestEnum2(Enum): + VAL1 = 'val1' + VAL2 = 'val2' + + enum1val: Annotated[ + TestEnum1, IOAttrs('e1', enum_fallback=TestEnum2.VAL1) + ] diff --git a/tools/bacommon/cloud.py b/tools/bacommon/cloud.py index 223fb714..a372f288 100644 --- a/tools/bacommon/cloud.py +++ b/tools/bacommon/cloud.py @@ -323,3 +323,12 @@ class BSPrivatePartyResponse(Response): tokens: Annotated[int, IOAttrs('t')] gold_pass: Annotated[bool, IOAttrs('g')] datacode: Annotated[str | None, IOAttrs('d')] + + +@ioprepped +@dataclass +class ClassicAccountData: + """Account related data for classic app mode.""" + + tokens: Annotated[int, IOAttrs('t')] + gold_pass: Annotated[bool, IOAttrs('g')] diff --git a/tools/efro/call.py b/tools/efro/call.py index 22eda0de..7f6fbe7b 100644 --- a/tools/efro/call.py +++ b/tools/efro/call.py @@ -17,10 +17,11 @@ if TYPE_CHECKING: class CallbackSet(Generic[T]): """A simple way to manage a set of callbacks. - Any number of calls can be added to a callback set. Each add results - in an entry that can be used to remove the call from the set later. - Callbacks are also implicitly removed when an entry is deallocated, - so make sure to hold on to the return value when adding. + Any number of calls can be registered with a callback set. Each + registration results in a Registration object that can be used to + deregister the call from the set later. Callbacks are also + implicitly deregistered when an entry is deallocated, so make sure + to hold on to the return value when adding. CallbackSet instances should be used from a single thread only (this will be checked in debug mode). @@ -32,8 +33,8 @@ class CallbackSet(Generic[T]): if __debug__: self.thread = threading.current_thread() - def add(self, call: T) -> CallbackRegistration[T]: - """Add a callback.""" + def register(self, call: T) -> CallbackRegistration[T]: + """Register a new callback.""" assert threading.current_thread() == self.thread self._prune() @@ -53,7 +54,8 @@ class CallbackSet(Generic[T]): self._prune() - # Ignore calls that have been deallocated or explicitly cleared. + # Ignore calls that have been deallocated or explicitly + # deregistered. entries = [e() for e in self._entries] return [e.call for e in entries if e is not None and e.call is not None] @@ -69,6 +71,7 @@ class CallbackSet(Generic[T]): if not needs_prune: return + # Ok; something needs pruning. Rebuild the entries list. newentries: list[weakref.ref[CallbackRegistration[T]]] = [] for entry in self._entries: entrytarget = entry() @@ -84,7 +87,7 @@ class CallbackRegistration(Generic[T]): self.call: T | None = call self.callbackset: CallbackSet[T] | None = callbackset - def clear(self) -> None: + def deregister(self) -> None: """Explicitly remove a callback from a CallbackSet.""" assert ( self.callbackset is None diff --git a/tools/efro/dataclassio/_base.py b/tools/efro/dataclassio/_base.py index d0743ab4..dc707377 100644 --- a/tools/efro/dataclassio/_base.py +++ b/tools/efro/dataclassio/_base.py @@ -160,6 +160,8 @@ class IOAttrs: fields; it should be used instead of 'soft_default' for mutable types such as lists to prevent a single default object from unintentionally changing over time. + 'enum_fallback', if provided, specifies an enum value to be substituted + in the case of unrecognized enum values. """ # A sentinel object to detect if a parameter is supplied or not. Use @@ -176,6 +178,7 @@ class IOAttrs: whole_minutes: bool = False soft_default: Any = MISSING soft_default_factory: Callable[[], Any] | _MissingType = MISSING + enum_fallback: Enum | None = None def __init__( self, @@ -187,6 +190,7 @@ class IOAttrs: whole_minutes: bool = whole_minutes, soft_default: Any = MISSING, soft_default_factory: Callable[[], Any] | _MissingType = MISSING, + enum_fallback: Enum | None = None, ): # Only store values that differ from class defaults to keep # our instances nice and lean. @@ -216,6 +220,8 @@ class IOAttrs: raise ValueError( 'Cannot set both soft_default and soft_default_factory' ) + if enum_fallback is not cls.enum_fallback: + self.enum_fallback = enum_fallback def validate_for_field(self, cls: type, field: dataclasses.Field) -> None: """Ensure the IOAttrs instance is ok to use with the provided field.""" diff --git a/tools/efro/dataclassio/_inputter.py b/tools/efro/dataclassio/_inputter.py index 1fb6ee72..9a975647 100644 --- a/tools/efro/dataclassio/_inputter.py +++ b/tools/efro/dataclassio/_inputter.py @@ -190,7 +190,16 @@ class _Inputter: ) if issubclass(origin, Enum): - return origin(value) + try: + return origin(value) + except ValueError: + # If a fallback enum was provided in ioattrs, return + # that for unrecognized values. + if ioattrs is not None and ioattrs.enum_fallback is not None: + assert type(ioattrs.enum_fallback) is origin + return ioattrs.enum_fallback + # Otherwise the error stands. + raise if issubclass(origin, datetime.datetime): return self._datetime_from_input(cls, fieldpath, value, ioattrs) diff --git a/tools/efro/dataclassio/_prep.py b/tools/efro/dataclassio/_prep.py index fd80bb4b..1f1e41c3 100644 --- a/tools/efro/dataclassio/_prep.py +++ b/tools/efro/dataclassio/_prep.py @@ -362,7 +362,7 @@ class PrepSession: pass elif issubclass(childtypes[0], Enum): # Allow our usual str or int enum types as keys. - self.prep_enum(childtypes[0]) + self.prep_enum(childtypes[0], ioattrs=None) else: raise TypeError( f'Dict key type {childtypes[0]} for \'{attrname}\'' @@ -412,7 +412,7 @@ class PrepSession: return if issubclass(origin, Enum): - self.prep_enum(origin) + self.prep_enum(origin, ioattrs=ioattrs) return # We allow datetime objects (and google's extended subclass of @@ -462,7 +462,11 @@ class PrepSession: recursion_level=recursion_level + 1, ) - def prep_enum(self, enumtype: type[Enum]) -> None: + def prep_enum( + self, + enumtype: type[Enum], + ioattrs: IOAttrs | None, + ) -> None: """Run prep on an enum type.""" valtype: Any = None @@ -485,3 +489,13 @@ class PrepSession: f' value types; dataclassio requires' f' them to be uniform.' ) + + if ioattrs is not None: + # If they provided a fallback enum value, make sure it + # is the correct type. + if ioattrs.enum_fallback is not None: + if type(ioattrs.enum_fallback) is not enumtype: + raise TypeError( + f'enum_fallback {ioattrs.enum_fallback} does not' + f' match the field type ({enumtype}.' + ) diff --git a/tools/efro/logging.py b/tools/efro/logging.py index 1bd73e6c..40fd984f 100644 --- a/tools/efro/logging.py +++ b/tools/efro/logging.py @@ -428,7 +428,7 @@ class LogHandler(logging.Handler): partial( logging.warning, 'efro.logging.LogHandler emit took too long' - ' (%.2fs total; %.2fs format, %.2fs echo,' + ' (%.3fs total; %.3fs format, %.3fs echo,' ' fast_path=%s).', duration, format_duration,