added enum fallback option to dataclassio

This commit is contained in:
Eric Froemling 2024-11-23 11:05:15 -08:00
parent bc808c65fc
commit 2cb0bf0f4f
No known key found for this signature in database
13 changed files with 219 additions and 94 deletions

72
.efrocachemap generated
View File

@ -4099,42 +4099,42 @@
"build/assets/windows/Win32/ucrtbased.dll": "bfd1180c269d3950b76f35a63655e9e1", "build/assets/windows/Win32/ucrtbased.dll": "bfd1180c269d3950b76f35a63655e9e1",
"build/assets/windows/Win32/vc_redist.x86.exe": "15a5f1f876503885adbdf5b3989b3718", "build/assets/windows/Win32/vc_redist.x86.exe": "15a5f1f876503885adbdf5b3989b3718",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "809ad854c769646c9bccf9f72bfd2f9f", "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "0921e644948438c78c8621f1199dc6d6",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "84f339cbec284e824904613e59cab1a2", "build/prefab/full/linux_arm64_gui/release/ballisticakit": "54486e895c6be0128a2e25624aaef051",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "2c385ed88eaf7787c4bf36b2681ccd91", "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "bcfc357760d1030c66e3b65c30a649c8",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "2156812371a87399fb3e0fff856fc739", "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "a398b7f179cf6ee31af8037bae59bdc4",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "40b5aacb91774c9343d674a15801f6d9", "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "9c5e37721e8834491c249b3f1e986e8a",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "8833e259a48cdb79a958d0ed18b56a65", "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "57dd4fa7356ea5a1a102c5a0422a4485",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "b5b8df8478e0d8513f6273d00bf97779", "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "cad5283f88be83233e7a8b674db02c17",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "e016a4a952ee8947169a2dfc34718a9f", "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "f36a3b1ef5d5650c373ac3230f3e64a3",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "fffe409a157aa345ba9193f992d2b752", "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "327abf1bd532b104cbb63fa9dd11f9b7",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "df686ee58c36af1100d20fa7a4deb08e", "build/prefab/full/mac_arm64_gui/release/ballisticakit": "685e787af247ba2694c7632c469e4853",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "70373843b698d5c4400a9cbb916d8d21", "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "6c41aad3f4c60d56350be66a6ba28a3f",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "f68567c5d2d5ea680c26578c2dc7a912", "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "d752bc8ee063e3d95038b3ce8bdb0e3c",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "71137c9d5d96923d4d5687de021488cd", "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "5f2e7c646ce28814d5d87ba2e4b826c4",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "f37c138bc5b6c17effa49159cc30db91", "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "b440e073afd70d018fc14ee10f41bb85",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "fecc9bbd4ebb50b633313077206c1504", "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "bc56615fbc3d671deebf36942b36c706",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "2d30efa541a0bd38fe578ce0baf929a6", "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "e5d47e94497951d438520cde86d487b2",
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "e8b47cf217d05ce0df5a334e3b456ced", "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "86eaadb3dee0a1f1137192fe943996f6",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "014014670516ce58a7d7dd3c39f12c84", "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "8c795d1a871f0d82a198b5aeb506d770",
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "e8b47cf217d05ce0df5a334e3b456ced", "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "86eaadb3dee0a1f1137192fe943996f6",
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "014014670516ce58a7d7dd3c39f12c84", "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "8c795d1a871f0d82a198b5aeb506d770",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "713c6046e9e061ac045c65f593a1dafb", "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "ab0cf0e9d6001748927660d84c1da87f",
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "107aa28e8e7568ff6fca408728b80d94", "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "62c22d7a25fd62831cb4a089bbee1b0b",
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "713c6046e9e061ac045c65f593a1dafb", "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "ab0cf0e9d6001748927660d84c1da87f",
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "107aa28e8e7568ff6fca408728b80d94", "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "62c22d7a25fd62831cb4a089bbee1b0b",
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "ffe19b0ae94217a0ed7ccc3d46c97a24", "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "75a5ab56d54304e602fff8dae6435904",
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "3f3a18dc4d6118ec181d03df1d0bd4de", "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "19b6e806d066affc80b32c4899c6ba2f",
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "ffe19b0ae94217a0ed7ccc3d46c97a24", "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "75a5ab56d54304e602fff8dae6435904",
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "3f3a18dc4d6118ec181d03df1d0bd4de", "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "19b6e806d066affc80b32c4899c6ba2f",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "6eeb79220b31acc56da748201090218f", "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "ca6096df15041b6af9a89ed81880c98b",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "ade915a77be68916691c0329f8cfdd9f", "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "f06c730c9cee3599679df778ec565842",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "67b55ef9f0d683340ef61c1df08de181", "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "1a77179b3548e78791ddd1205ff505a3",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "689fd22835bb965b5e7fafe5149f8319", "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "d755dc558bba71715ed0b543fa966be4",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "fdc1fa0dbaca6e517f2608bcc7476e96", "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "722aff67a010cc0a435bc70018b0b49a",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "6af6ad18c0486410454db2a53b72e458", "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "373deb2c02283978f415c1b937229292",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "f496932bed2c235654829404741b4526", "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "fe26aafc763a1198e1921caeb261bdb8",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "5ec6c0c883ca79b2eec1f7b952ecaa39", "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/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "794d258d59fd17a61752843a9a0551ad", "src/assets/ba_data/python/babase/_mgen/enums.py": "794d258d59fd17a61752843a9a0551ad",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "3a583e7e03bd4907b21adc3bf5729d15", "src/ballistica/base/mgen/pyembed/binding_base.inc": "3a583e7e03bd4907b21adc3bf5729d15",

View File

@ -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 - 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 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. touch UI stuff at all you can simply bump your api version and call it a day.

View File

@ -25,6 +25,7 @@ if TYPE_CHECKING:
from typing import Callable, Any from typing import Callable, Any
from efro.call import CallbackRegistration from efro.call import CallbackRegistration
from bacommon.cloud import ClassicAccountData
from babase import AppIntent, AccountV2Handle, CloudSubscription from babase import AppIntent, AccountV2Handle, CloudSubscription
from bauiv1 import UIV1AppSubsystem, MainWindow, MainWindowState from bauiv1 import UIV1AppSubsystem, MainWindow, MainWindowState
@ -38,6 +39,7 @@ class ClassicAppMode(AppMode):
CallbackRegistration | None CallbackRegistration | None
) = None ) = None
self._test_sub: CloudSubscription | None = None self._test_sub: CloudSubscription | None = None
self._account_data_sub: CloudSubscription | None = None
@override @override
@classmethod @classmethod
@ -122,7 +124,7 @@ class ClassicAppMode(AppMode):
# We want to be informed when primary account changes. # We want to be informed when primary account changes.
self._on_primary_account_changed_callback = ( 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 self.update_for_primary_account
) )
) )
@ -135,7 +137,7 @@ class ClassicAppMode(AppMode):
# Stop being informed of account changes. # Stop being informed of account changes.
self._on_primary_account_changed_callback = None 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) self.update_for_primary_account(None)
# Save where we were in the UI so we return there next time. # Save where we were in the UI so we return there next time.
@ -157,10 +159,10 @@ class ClassicAppMode(AppMode):
) -> None: ) -> None:
"""Update subscriptions/etc. for a new primary account state.""" """Update subscriptions/etc. for a new primary account state."""
assert in_logic_thread() assert in_logic_thread()
assert app.plus is not None
# For testing subscription functionality. # For testing subscription functionality.
if os.environ.get('BA_SUBSCRIPTION_TEST') == '1': if os.environ.get('BA_SUBSCRIPTION_TEST') == '1':
assert app.plus is not None
if account is None: if account is None:
self._test_sub = None self._test_sub = None
else: else:
@ -171,9 +173,22 @@ class ClassicAppMode(AppMode):
else: else:
self._test_sub = None 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: def _on_sub_test_update(self, val: int | None) -> None:
print(f'GOT SUB TEST UPDATE: {val}') 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: def _root_ui_menu_press(self) -> None:
from babase import push_back_press from babase import push_back_press
@ -184,7 +199,6 @@ class ClassicAppMode(AppMode):
if old_window is not None: if old_window is not None:
classic = app.classic classic = app.classic
assert classic is not None assert classic is not None
classic.resume() classic.resume()
@ -230,7 +244,13 @@ class ClassicAppMode(AppMode):
win_type: type[MainWindow], win_type: type[MainWindow],
win_create_call: Callable[[], MainWindow], win_create_call: Callable[[], MainWindow],
) -> None: ) -> 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 # pylint: disable=unidiomatic-typecheck
ui = app.ui_v1 ui = app.ui_v1
@ -286,8 +306,8 @@ class ClassicAppMode(AppMode):
) )
return return
# Ok, no auxiliary states found. Now if current window is auxiliary # Ok, no auxiliary states found. Now if current window is
# and the type matches, simply do a back. # auxiliary and the type matches, simply do a back.
if ( if (
current_main_window.main_window_is_auxiliary current_main_window.main_window_is_auxiliary
and type(current_main_window) is win_type and type(current_main_window) is win_type

View File

@ -53,7 +53,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be # Build number and version of the ballistica binary we expect to be
# using. # using.
TARGET_BALLISTICA_BUILD = 22110 TARGET_BALLISTICA_BUILD = 22112
TARGET_BALLISTICA_VERSION = '1.7.37' TARGET_BALLISTICA_VERSION = '1.7.37'

View File

@ -177,7 +177,15 @@ class CloudSubsystem(babase.AppSubsystem):
def subscribe_test( def subscribe_test(
self, updatecall: Callable[[int | None], None] self, updatecall: Callable[[int | None], None]
) -> babase.CloudSubscription: ) -> 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( raise NotImplementedError(
'Cloud functionality is not present in this build.' 'Cloud functionality is not present in this build.'
) )

View File

@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't modify them here. // 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 char* kEngineVersion = "1.7.37";
const int kEngineApiVersion = 9; const int kEngineApiVersion = 9;

View File

@ -588,7 +588,8 @@ def test_dict() -> None:
obj = _TestClass(dval={}) 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 obj.dval['foo'] = 5
dataclass_to_dict(obj) dataclass_to_dict(obj)
with pytest.raises(TypeError): with pytest.raises(TypeError):
@ -598,8 +599,8 @@ def test_dict() -> None:
obj.dval['foo'] = _GoodEnum.VAL1 obj.dval['foo'] = _GoodEnum.VAL1
dataclass_to_dict(obj) dataclass_to_dict(obj)
# Int dict-keys should actually be stored as strings internally # Int dict-keys should actually be stored as strings internally (for
# (for json compatibility). # json compatibility).
@ioprepped @ioprepped
@dataclass @dataclass
class _TestClass2: class _TestClass2:
@ -753,8 +754,8 @@ def test_any() -> None:
obj = _TestClass(anyval=b'bytes') obj = _TestClass(anyval=b'bytes')
# JSON output doesn't allow bytes or datetime objects # JSON output doesn't allow bytes or datetime objects included in
# included in 'Any' data. # 'Any' data.
with pytest.raises(TypeError): with pytest.raises(TypeError):
dataclass_validate(obj, codec=Codec.JSON) dataclass_validate(obj, codec=Codec.JSON)
@ -831,8 +832,8 @@ def test_datetime_limits() -> None:
def test_field_paths() -> None: def test_field_paths() -> None:
"""Test type-safe field path evaluations.""" """Test type-safe field path evaluations."""
# Define a few nested dataclass types, some of which # Define a few nested dataclass types, some of which have storage
# have storage names differing from their field names. # names differing from their field names.
@ioprepped @ioprepped
@dataclass @dataclass
class _TestClass: 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.val1) == 's2.val1'
assert lookup.path(lambda obj: obj.sub2.val2) == 's2.v2' assert lookup.path(lambda obj: obj.sub2.val2) == 's2.v2'
# Attempting to return fields that aren't there should fail # Attempting to return fields that aren't there should fail in both
# in both type-checking and runtime. # type-checking and runtime.
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
lookup.path(lambda obj: obj.sub1.val3) # type: ignore lookup.path(lambda obj: obj.sub1.val3) # type: ignore
# Returning non-field objects will fail at runtime # Returning non-field objects will fail at runtime even if
# even if type-checking evaluates them as valid values. # type-checking evaluates them as valid values.
with pytest.raises(TypeError): with pytest.raises(TypeError):
lookup.path(lambda obj: 1) lookup.path(lambda obj: 1)
@ -905,8 +906,8 @@ def test_extended_data() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
_obj = dataclass_from_dict(_TestClass, indata) _obj = dataclass_from_dict(_TestClass, indata)
# Now define the same data but give it an adapter # Now define the same data but give it an adapter so it can work
# so it can work with our incorrectly-formatted data. # with our incorrectly-formatted data.
@ioprepped @ioprepped
@dataclass @dataclass
class _TestClass2(IOExtendedData): class _TestClass2(IOExtendedData):
@ -978,8 +979,8 @@ def test_soft_default() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
dataclass_from_dict(_TestClassA2, {}) dataclass_from_dict(_TestClassA2, {})
# These should succeed because it has a soft-default value to # These should succeed because it has a soft-default value to fall
# fall back on. # back on.
dataclass_from_dict(_TestClassB, {}) dataclass_from_dict(_TestClassB, {})
dataclass_from_dict(_TestClassB2, {}) dataclass_from_dict(_TestClassB2, {})
dataclass_from_dict(_TestClassB3, {}) dataclass_from_dict(_TestClassB3, {})
@ -1020,8 +1021,8 @@ def test_soft_default() -> None:
assert dataclass_to_dict(_TestClassC3b(0)) == {} assert dataclass_to_dict(_TestClassC3b(0)) == {}
# We disallow passing a few mutable types as soft_defaults # We disallow passing a few mutable types as soft_defaults just as
# just as dataclass does with regular defaults. # dataclass does with regular defaults.
with pytest.raises(TypeError): with pytest.raises(TypeError):
@ioprepped @ioprepped
@ -1044,9 +1045,9 @@ def test_soft_default() -> None:
class _TestClassD3: class _TestClassD3:
lval: Annotated[dict, IOAttrs(soft_default={})] lval: Annotated[dict, IOAttrs(soft_default={})]
# soft_defaults are not static-type-checked, but we do try to # soft_defaults are not static-type-checked, but we do try to catch
# catch basic type mismatches at prep time. Make sure that's working. # basic type mismatches at prep time. Make sure that's working. (we
# (we also do full value validation during input, but the more we catch # also do full value validation during input, but the more we catch
# early the better) # early the better)
with pytest.raises(TypeError): with pytest.raises(TypeError):
@ -1069,9 +1070,9 @@ def test_soft_default() -> None:
class _TestClassE3: class _TestClassE3:
lval: Annotated[list, IOAttrs(soft_default_factory=set)] lval: Annotated[list, IOAttrs(soft_default_factory=set)]
# Make sure Unions/Optionals go through ok. # Make sure Unions/Optionals go through ok. Note that mismatches
# (note that mismatches currently aren't caught at prep time; just # currently aren't caught at prep time; just checking the negative
# checking the negative case here). # case here.
@ioprepped @ioprepped
@dataclass @dataclass
class _TestClassE4: class _TestClassE4:
@ -1083,7 +1084,8 @@ def test_soft_default() -> None:
lval: Annotated[str | None, IOAttrs(soft_default='foo')] lval: Annotated[str | None, IOAttrs(soft_default='foo')]
# Now try more in-depth examples: nested type mismatches like this # 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 @ioprepped
@dataclass @dataclass
class _TestClassE6: class _TestClassE6:
@ -1100,9 +1102,9 @@ def test_soft_default() -> None:
with pytest.raises(TypeError): with pytest.raises(TypeError):
dataclass_from_dict(_TestClassE7, {}) dataclass_from_dict(_TestClassE7, {})
# If both a soft_default and regular field default are present, # If both a soft_default and regular field default are present, make
# make sure soft_default takes precedence (it applies before # sure soft_default takes precedence (it applies before data even
# data even hits the dataclass constructor). # hits the dataclass constructor).
@ioprepped @ioprepped
@dataclass @dataclass
@ -1111,8 +1113,8 @@ def test_soft_default() -> None:
assert dataclass_from_dict(_TestClassE8, {}).ival == 1 assert dataclass_from_dict(_TestClassE8, {}).ival == 1
# Make sure soft_default gets used both when determining when # Make sure soft_default gets used both when determining when to
# to omit values from output and what to recreate missing values as. # omit values from output and what to recreate missing values as.
orig = _TestClassE8(ival=1) orig = _TestClassE8(ival=1)
todict = dataclass_to_dict(orig) todict = dataclass_to_dict(orig)
assert todict == {} assert todict == {}
@ -1168,11 +1170,10 @@ class MTTestBase(IOMultiType[MTTestTypeID]):
@classmethod @classmethod
def get_type_id(cls) -> MTTestTypeID: def get_type_id(cls) -> MTTestTypeID:
"""Provide the type-id for this subclass.""" """Provide the type-id for this subclass."""
# If we wanted, we could just maintain a static mapping # If we wanted, we could just maintain a static mapping of
# of types-to-ids here, but there are benefits to letting # types-to-ids here, but there are benefits to letting each
# each child class speak for itself. Namely that we can # child class speak for itself. Namely that we can do
# do lazy-loading and don't need to have all types present # lazy-loading and don't need to have all types present here.
# here.
# So we'll let all our child classes override this. # So we'll let all our child classes override this.
raise NotImplementedError() raise NotImplementedError()
@ -1457,3 +1458,58 @@ def test_multi_type_2() -> None:
indict3 = {'type': 'm3'} indict3 = {'type': 'm3'}
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
val3 = dataclass_from_dict(MTTest2Base, indict3) 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)
]

View File

@ -323,3 +323,12 @@ class BSPrivatePartyResponse(Response):
tokens: Annotated[int, IOAttrs('t')] tokens: Annotated[int, IOAttrs('t')]
gold_pass: Annotated[bool, IOAttrs('g')] gold_pass: Annotated[bool, IOAttrs('g')]
datacode: Annotated[str | None, IOAttrs('d')] 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')]

View File

@ -17,10 +17,11 @@ if TYPE_CHECKING:
class CallbackSet(Generic[T]): class CallbackSet(Generic[T]):
"""A simple way to manage a set of callbacks. """A simple way to manage a set of callbacks.
Any number of calls can be added to a callback set. Each add results Any number of calls can be registered with a callback set. Each
in an entry that can be used to remove the call from the set later. registration results in a Registration object that can be used to
Callbacks are also implicitly removed when an entry is deallocated, deregister the call from the set later. Callbacks are also
so make sure to hold on to the return value when adding. 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 CallbackSet instances should be used from a single thread only
(this will be checked in debug mode). (this will be checked in debug mode).
@ -32,8 +33,8 @@ class CallbackSet(Generic[T]):
if __debug__: if __debug__:
self.thread = threading.current_thread() self.thread = threading.current_thread()
def add(self, call: T) -> CallbackRegistration[T]: def register(self, call: T) -> CallbackRegistration[T]:
"""Add a callback.""" """Register a new callback."""
assert threading.current_thread() == self.thread assert threading.current_thread() == self.thread
self._prune() self._prune()
@ -53,7 +54,8 @@ class CallbackSet(Generic[T]):
self._prune() 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] 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] 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: if not needs_prune:
return return
# Ok; something needs pruning. Rebuild the entries list.
newentries: list[weakref.ref[CallbackRegistration[T]]] = [] newentries: list[weakref.ref[CallbackRegistration[T]]] = []
for entry in self._entries: for entry in self._entries:
entrytarget = entry() entrytarget = entry()
@ -84,7 +87,7 @@ class CallbackRegistration(Generic[T]):
self.call: T | None = call self.call: T | None = call
self.callbackset: CallbackSet[T] | None = callbackset self.callbackset: CallbackSet[T] | None = callbackset
def clear(self) -> None: def deregister(self) -> None:
"""Explicitly remove a callback from a CallbackSet.""" """Explicitly remove a callback from a CallbackSet."""
assert ( assert (
self.callbackset is None self.callbackset is None

View File

@ -160,6 +160,8 @@ class IOAttrs:
fields; it should be used instead of 'soft_default' for mutable types fields; it should be used instead of 'soft_default' for mutable types
such as lists to prevent a single default object from unintentionally such as lists to prevent a single default object from unintentionally
changing over time. 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 # A sentinel object to detect if a parameter is supplied or not. Use
@ -176,6 +178,7 @@ class IOAttrs:
whole_minutes: bool = False whole_minutes: bool = False
soft_default: Any = MISSING soft_default: Any = MISSING
soft_default_factory: Callable[[], Any] | _MissingType = MISSING soft_default_factory: Callable[[], Any] | _MissingType = MISSING
enum_fallback: Enum | None = None
def __init__( def __init__(
self, self,
@ -187,6 +190,7 @@ class IOAttrs:
whole_minutes: bool = whole_minutes, whole_minutes: bool = whole_minutes,
soft_default: Any = MISSING, soft_default: Any = MISSING,
soft_default_factory: Callable[[], Any] | _MissingType = MISSING, soft_default_factory: Callable[[], Any] | _MissingType = MISSING,
enum_fallback: Enum | None = None,
): ):
# Only store values that differ from class defaults to keep # Only store values that differ from class defaults to keep
# our instances nice and lean. # our instances nice and lean.
@ -216,6 +220,8 @@ class IOAttrs:
raise ValueError( raise ValueError(
'Cannot set both soft_default and soft_default_factory' '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: def validate_for_field(self, cls: type, field: dataclasses.Field) -> None:
"""Ensure the IOAttrs instance is ok to use with the provided field.""" """Ensure the IOAttrs instance is ok to use with the provided field."""

View File

@ -190,7 +190,16 @@ class _Inputter:
) )
if issubclass(origin, Enum): 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): if issubclass(origin, datetime.datetime):
return self._datetime_from_input(cls, fieldpath, value, ioattrs) return self._datetime_from_input(cls, fieldpath, value, ioattrs)

View File

@ -362,7 +362,7 @@ class PrepSession:
pass pass
elif issubclass(childtypes[0], Enum): elif issubclass(childtypes[0], Enum):
# Allow our usual str or int enum types as keys. # Allow our usual str or int enum types as keys.
self.prep_enum(childtypes[0]) self.prep_enum(childtypes[0], ioattrs=None)
else: else:
raise TypeError( raise TypeError(
f'Dict key type {childtypes[0]} for \'{attrname}\'' f'Dict key type {childtypes[0]} for \'{attrname}\''
@ -412,7 +412,7 @@ class PrepSession:
return return
if issubclass(origin, Enum): if issubclass(origin, Enum):
self.prep_enum(origin) self.prep_enum(origin, ioattrs=ioattrs)
return return
# We allow datetime objects (and google's extended subclass of # We allow datetime objects (and google's extended subclass of
@ -462,7 +462,11 @@ class PrepSession:
recursion_level=recursion_level + 1, 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.""" """Run prep on an enum type."""
valtype: Any = None valtype: Any = None
@ -485,3 +489,13 @@ class PrepSession:
f' value types; dataclassio requires' f' value types; dataclassio requires'
f' them to be uniform.' 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}.'
)

View File

@ -428,7 +428,7 @@ class LogHandler(logging.Handler):
partial( partial(
logging.warning, logging.warning,
'efro.logging.LogHandler emit took too long' 'efro.logging.LogHandler emit took too long'
' (%.2fs total; %.2fs format, %.2fs echo,' ' (%.3fs total; %.3fs format, %.3fs echo,'
' fast_path=%s).', ' fast_path=%s).',
duration, duration,
format_duration, format_duration,