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/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",

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
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.

View File

@ -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

View File

@ -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'

View File

@ -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.'
)

View File

@ -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;

View File

@ -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)
]

View File

@ -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')]

View File

@ -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

View File

@ -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."""

View File

@ -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)

View File

@ -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}.'
)

View File

@ -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,