ballistica/tests/test_efro/test_dataclassio.py
Eric Froemling 4d31d59467
v1.6.7
2022-01-25 14:25:45 -06:00

876 lines
26 KiB
Python

# Released under the MIT License. See LICENSE for details.
#
"""Testing dataclasses functionality."""
from __future__ import annotations
from enum import Enum
import datetime
from dataclasses import field, dataclass
from typing import TYPE_CHECKING, Optional, Any, Sequence, Union, Annotated
import pytest
from efro.util import utc_now
from efro.dataclassio import (dataclass_validate, dataclass_from_dict,
dataclass_to_dict, ioprepped, ioprep, IOAttrs,
Codec, DataclassFieldLookup, IOExtendedData)
if TYPE_CHECKING:
pass
class _EnumTest(Enum):
TEST1 = 'test1'
TEST2 = 'test2'
class _GoodEnum(Enum):
VAL1 = 'val1'
VAL2 = 'val2'
class _GoodEnum2(Enum):
VAL1 = 1
VAL2 = 2
class _BadEnum1(Enum):
VAL1 = 1.23
class _BadEnum2(Enum):
VAL1 = 1
VAL2 = 'val2'
@dataclass
class _NestedClass:
ival: int = 0
sval: str = 'foo'
dval: dict[int, str] = field(default_factory=dict)
def test_assign() -> None:
"""Testing various assignments."""
# pylint: disable=too-many-statements
@ioprepped
@dataclass
class _TestClass:
ival: int = 0
sval: str = ''
bval: bool = True
fval: float = 1.0
nval: _NestedClass = field(default_factory=_NestedClass)
enval: _EnumTest = _EnumTest.TEST1
oival: Optional[int] = None
osval: Optional[str] = None
obval: Optional[bool] = None
ofval: Optional[float] = None
oenval: Optional[_EnumTest] = _EnumTest.TEST1
lsval: list[str] = field(default_factory=list)
lival: list[int] = field(default_factory=list)
lbval: list[bool] = field(default_factory=list)
lfval: list[float] = field(default_factory=list)
lenval: list[_EnumTest] = field(default_factory=list)
ssval: set[str] = field(default_factory=set)
anyval: Any = 1
dictval: dict[int, str] = field(default_factory=dict)
tupleval: tuple[int, str, bool] = (1, 'foo', False)
datetimeval: Optional[datetime.datetime] = None
class _TestClass2:
pass
# Attempting to use with non-dataclass should fail.
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass2, {})
# Attempting to pass non-dicts should fail.
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, []) # type: ignore
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, None) # type: ignore
now = utc_now()
# A dict containing *ALL* values should match what we
# get when creating a dataclass and then converting back
# to a dict.
dict1 = {
'ival':
1,
'sval':
'foo',
'bval':
True,
'fval':
2.0,
'nval': {
'ival': 1,
'sval': 'bar',
'dval': {
'1': 'foof'
},
},
'enval':
'test1',
'oival':
1,
'osval':
'foo',
'obval':
True,
'ofval':
1.0,
'oenval':
'test2',
'lsval': ['foo'],
'lival': [10],
'lbval': [False],
'lfval': [1.0],
'lenval': ['test1', 'test2'],
'ssval': ['foo'],
'dval': {
'k': 123
},
'anyval': {
'foo': [1, 2, {
'bar': 'eep',
'rah': 1
}]
},
'dictval': {
'1': 'foo'
},
'tupleval': [2, 'foof', True],
'datetimeval': [
now.year, now.month, now.day, now.hour, now.minute, now.second,
now.microsecond
],
}
dc1 = dataclass_from_dict(_TestClass, dict1)
assert dataclass_to_dict(dc1) == dict1
# A few other assignment checks.
assert isinstance(
dataclass_from_dict(
_TestClass, {
'oival': None,
'osval': None,
'obval': None,
'ofval': None,
'lsval': [],
'lival': [],
'lbval': [],
'lfval': [],
'ssval': []
}), _TestClass)
assert isinstance(
dataclass_from_dict(
_TestClass, {
'oival': 1,
'osval': 'foo',
'obval': True,
'ofval': 2.0,
'lsval': ['foo', 'bar', 'eep'],
'lival': [10, 11, 12],
'lbval': [False, True],
'lfval': [1.0, 2.0, 3.0]
}), _TestClass)
# Attr assigns mismatched with their value types should fail.
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ival': 'foo'})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'sval': 1})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'bval': 2})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'oival': 'foo'})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'osval': 1})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'obval': 2})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ofval': 'blah'})
with pytest.raises(ValueError):
dataclass_from_dict(_TestClass, {'oenval': 'test3'})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lsval': 'blah'})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lsval': ['blah', None]})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lsval': [1]})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lsval': (1, )})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lbval': [None]})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lival': ['foo']})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lfval': [True]})
with pytest.raises(ValueError):
dataclass_from_dict(_TestClass, {'lenval': ['test1', 'test3']})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ssval': [True]})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ssval': {}})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ssval': set()})
with pytest.raises(ValueError):
dataclass_from_dict(_TestClass, {'tupleval': []})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'tupleval': [1, 1, 1]})
with pytest.raises(ValueError):
dataclass_from_dict(_TestClass, {'tupleval': [2, 'foof', True, True]})
# Fields with type Any should accept all types which are directly
# supported by json, but not ones such as tuples or non-string dict keys
# which get implicitly translated by python's json module.
dataclass_from_dict(_TestClass, {'anyval': {}})
dataclass_from_dict(_TestClass, {'anyval': None})
dataclass_from_dict(_TestClass, {'anyval': []})
dataclass_from_dict(_TestClass, {'anyval': [True, {'foo': 'bar'}, None]})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'anyval': {1: 'foo'}})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'anyval': set()})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'anyval': (1, 2, 3)})
# More subtle attr/type mismatches that should fail
# (we currently require EXACT type matches).
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ival': True})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'fval': 2}, coerce_to_float=False)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'bval': 1})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ofval': 1}, coerce_to_float=False)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lfval': [1]}, coerce_to_float=False)
# Coerce-to-float should only work on ints; not bools or other types.
dataclass_from_dict(_TestClass, {'fval': 1}, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'fval': 1}, coerce_to_float=False)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'fval': True}, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'fval': None}, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'fval': []}, coerce_to_float=True)
# Datetime values should only be allowed with timezone set as utc.
dataclass_to_dict(_TestClass(datetimeval=utc_now()))
with pytest.raises(ValueError):
dataclass_to_dict(_TestClass(datetimeval=datetime.datetime.now()))
with pytest.raises(ValueError):
# This doesn't actually set timezone on the datetime obj.
dataclass_to_dict(_TestClass(datetimeval=datetime.datetime.utcnow()))
def test_coerce() -> None:
"""Test value coercion."""
@ioprepped
@dataclass
class _TestClass:
ival: int = 0
fval: float = 0.0
# Float value present for int should never work.
obj = _TestClass()
# noinspection PyTypeHints
obj.ival = 1.0 # type: ignore
with pytest.raises(TypeError):
dataclass_validate(obj, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_validate(obj, coerce_to_float=False)
# Int value present for float should work only with coerce on.
obj = _TestClass()
obj.fval = 1
dataclass_validate(obj, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_validate(obj, coerce_to_float=False)
# Likewise, passing in an int for a float field should work only
# with coerce on.
dataclass_from_dict(_TestClass, {'fval': 1}, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'fval': 1}, coerce_to_float=False)
# Passing in floats for an int field should never work.
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ival': 1.0}, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ival': 1.0}, coerce_to_float=False)
def test_prep() -> None:
"""Test the prepping process."""
# We currently don't support Sequence; can revisit if there is
# a strong use case.
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass:
ival: Sequence[int]
# We currently only support Unions with exactly 2 members; one of
# which is None. (Optional types get transformed into this by
# get_type_hints() so we need to support at least that).
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass2:
ival: Union[int, str]
@ioprepped
@dataclass
class _TestClass3:
uval: Union[int, None]
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass4:
ival: Union[int, str]
# This will get simplified down to simply int by get_type_hints so is ok.
@ioprepped
@dataclass
class _TestClass5:
ival: Union[int]
# This will get simplified down to a valid 2 member union so is ok
@ioprepped
@dataclass
class _TestClass6:
ival: Union[int, None, int, None]
# Disallow dict entries with types other than str, int, or enums
# having those value types.
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass7:
dval: dict[float, int]
@ioprepped
@dataclass
class _TestClass8:
dval: dict[str, int]
@ioprepped
@dataclass
class _TestClass9:
dval: dict[_GoodEnum, int]
@ioprepped
@dataclass
class _TestClass10:
dval: dict[_GoodEnum2, int]
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass11:
dval: dict[_BadEnum1, int]
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass12:
dval: dict[_BadEnum2, int]
def test_validate() -> None:
"""Testing validation."""
@ioprepped
@dataclass
class _TestClass:
ival: int = 0
sval: str = ''
bval: bool = True
fval: float = 1.0
oival: Optional[int] = None
osval: Optional[str] = None
obval: Optional[bool] = None
ofval: Optional[float] = None
# Should pass by default.
tclass = _TestClass()
dataclass_validate(tclass)
# No longer valid (without coerce)
tclass.fval = 1
with pytest.raises(TypeError):
dataclass_validate(tclass, coerce_to_float=False)
# Should pass by default.
tclass = _TestClass()
dataclass_validate(tclass)
# No longer valid.
# noinspection PyTypeHints
tclass.ival = None # type: ignore
with pytest.raises(TypeError):
dataclass_validate(tclass)
def test_extra_data() -> None:
"""Test handling of data that doesn't map to dataclass attrs."""
@ioprepped
@dataclass
class _TestClass:
ival: int = 0
sval: str = ''
# Passing an attr not in the dataclass should fail if we ask it to.
with pytest.raises(AttributeError):
dataclass_from_dict(_TestClass, {'nonexistent': 'foo'},
allow_unknown_attrs=False)
# But normally it should be preserved and present in re-export.
obj = dataclass_from_dict(_TestClass, {'nonexistent': 'foo'})
assert isinstance(obj, _TestClass)
out = dataclass_to_dict(obj)
assert out.get('nonexistent') == 'foo'
# But not if we ask it to discard unknowns.
obj = dataclass_from_dict(_TestClass, {'nonexistent': 'foo'},
discard_unknown_attrs=True)
assert isinstance(obj, _TestClass)
out = dataclass_to_dict(obj)
assert 'nonexistent' not in out
def test_ioattrs() -> None:
"""Testing ioattrs annotations."""
@ioprepped
@dataclass
class _TestClass:
dval: Annotated[dict, IOAttrs('d')]
obj = _TestClass(dval={'foo': 'bar'})
# Make sure key is working.
assert dataclass_to_dict(obj) == {'d': {'foo': 'bar'}}
# Setting store_default False without providing a default or
# default_factory should fail.
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass2:
dval: Annotated[dict, IOAttrs('d', store_default=False)]
@ioprepped
@dataclass
class _TestClass3:
dval: Annotated[dict, IOAttrs('d', store_default=False)] = field(
default_factory=dict)
ival: Annotated[int, IOAttrs('i', store_default=False)] = 123
# Both attrs are default; should get stripped out.
obj3 = _TestClass3()
assert dataclass_to_dict(obj3) == {}
# Both attrs are non-default vals; should remain in output.
obj3 = _TestClass3(dval={'foo': 'bar'}, ival=124)
assert dataclass_to_dict(obj3) == {'d': {'foo': 'bar'}, 'i': 124}
# Test going the other way.
obj3 = dataclass_from_dict(
_TestClass3,
{
'd': {
'foo': 'barf'
},
'i': 125
},
allow_unknown_attrs=False,
)
assert obj3.dval == {'foo': 'barf'}
assert obj3.ival == 125
def test_codecs() -> None:
"""Test differences with codecs."""
@ioprepped
@dataclass
class _TestClass:
bval: bytes
# bytes to/from JSON (goes through base64)
obj = _TestClass(bval=b'foo')
out = dataclass_to_dict(obj, codec=Codec.JSON)
assert isinstance(out['bval'], str) and out['bval'] == 'Zm9v'
obj = dataclass_from_dict(_TestClass, out, codec=Codec.JSON)
assert obj.bval == b'foo'
# bytes to/from FIRESTORE (passed as-is)
obj = _TestClass(bval=b'foo')
out = dataclass_to_dict(obj, codec=Codec.FIRESTORE)
assert isinstance(out['bval'], bytes) and out['bval'] == b'foo'
obj = dataclass_from_dict(_TestClass, out, codec=Codec.FIRESTORE)
assert obj.bval == b'foo'
now = utc_now()
@ioprepped
@dataclass
class _TestClass2:
dval: datetime.datetime
# datetime to/from JSON (turns into a list of values)
obj2 = _TestClass2(dval=now)
out = dataclass_to_dict(obj2, codec=Codec.JSON)
assert (isinstance(out['dval'], list) and len(out['dval']) == 7
and all(isinstance(val, int) for val in out['dval']))
obj2 = dataclass_from_dict(_TestClass2, out, codec=Codec.JSON)
assert obj2.dval == now
# datetime to/from FIRESTORE (passed through as-is)
obj2 = _TestClass2(dval=now)
out = dataclass_to_dict(obj2, codec=Codec.FIRESTORE)
assert isinstance(out['dval'], datetime.datetime)
obj2 = dataclass_from_dict(_TestClass2, out, codec=Codec.FIRESTORE)
assert obj2.dval == now
def test_dict() -> None:
"""Test various dict related bits."""
@ioprepped
@dataclass
class _TestClass:
dval: dict
obj = _TestClass(dval={})
# 'Any' dicts should only support values directly compatible with json.
obj.dval['foo'] = 5
dataclass_to_dict(obj)
with pytest.raises(TypeError):
obj.dval[5] = 5
dataclass_to_dict(obj)
with pytest.raises(TypeError):
obj.dval['foo'] = _GoodEnum.VAL1
dataclass_to_dict(obj)
# Int dict-keys should actually be stored as strings internally
# (for json compatibility).
@ioprepped
@dataclass
class _TestClass2:
dval: dict[int, float]
obj2 = _TestClass2(dval={1: 2.34})
out = dataclass_to_dict(obj2)
assert '1' in out['dval']
assert 1 not in out['dval']
out['dval']['1'] = 2.35
obj2 = dataclass_from_dict(_TestClass2, out)
assert isinstance(obj2, _TestClass2)
assert obj2.dval[1] == 2.35
# Same with enum keys (we support enums with str and int values)
@ioprepped
@dataclass
class _TestClass3:
dval: dict[_GoodEnum, int]
obj3 = _TestClass3(dval={_GoodEnum.VAL1: 123})
out = dataclass_to_dict(obj3)
assert out['dval']['val1'] == 123
out['dval']['val1'] = 124
obj3 = dataclass_from_dict(_TestClass3, out)
assert obj3.dval[_GoodEnum.VAL1] == 124
@ioprepped
@dataclass
class _TestClass4:
dval: dict[_GoodEnum2, int]
obj4 = _TestClass4(dval={_GoodEnum2.VAL1: 125})
out = dataclass_to_dict(obj4)
assert out['dval']['1'] == 125
out['dval']['1'] = 126
obj4 = dataclass_from_dict(_TestClass4, out)
assert obj4.dval[_GoodEnum2.VAL1] == 126
# The wrong enum type as a key should error.
obj4.dval = {_GoodEnum.VAL1: 999} # type: ignore
with pytest.raises(TypeError):
dataclass_to_dict(obj4)
def test_name_clashes() -> None:
"""Make sure we catch name clashes since we can remap attr names."""
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass:
ival: Annotated[int, IOAttrs('i')] = 4
ival2: Annotated[int, IOAttrs('i')] = 5
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass2:
ival: int = 4
ival2: Annotated[int, IOAttrs('ival')] = 5
with pytest.raises(TypeError):
@ioprepped
@dataclass
class _TestClass3:
ival: Annotated[int, IOAttrs(store_default=False)] = 4
ival2: Annotated[int, IOAttrs('ival')] = 5
@dataclass
class _RecursiveTest:
val: int
child: Optional[_RecursiveTest] = None
def test_recursive() -> None:
"""Test recursive classes."""
# Can't use ioprepped on this since it refers to its own name which
# doesn't exist yet. Have to explicitly prep it after.
ioprep(_RecursiveTest)
rtest = _RecursiveTest(val=1)
rtest.child = _RecursiveTest(val=2)
rtest.child.child = _RecursiveTest(val=3)
expected_output = {
'val': 1,
'child': {
'val': 2,
'child': {
'val': 3,
'child': None
}
}
}
assert dataclass_to_dict(rtest) == expected_output
assert dataclass_from_dict(_RecursiveTest, expected_output) == rtest
def test_any() -> None:
"""Test data included with type Any."""
@ioprepped
@dataclass
class _TestClass:
anyval: Any
obj = _TestClass(anyval=b'bytes')
# JSON output doesn't allow bytes or datetime objects
# included in 'Any' data.
with pytest.raises(TypeError):
dataclass_validate(obj, codec=Codec.JSON)
obj.anyval = datetime.datetime.now()
with pytest.raises(TypeError):
dataclass_validate(obj, codec=Codec.JSON)
# Firestore, however, does.
obj.anyval = b'bytes'
dataclass_validate(obj, codec=Codec.FIRESTORE)
obj.anyval = datetime.datetime.now()
dataclass_validate(obj, codec=Codec.FIRESTORE)
@ioprepped
@dataclass
class _SPTestClass1:
barf: int = 5
eep: str = 'blah'
barf2: Annotated[int, IOAttrs('b')] = 5
@ioprepped
@dataclass
class _SPTestClass2:
rah: bool = False
subc: _SPTestClass1 = field(default_factory=_SPTestClass1)
subc2: Annotated[_SPTestClass1,
IOAttrs('s')] = field(default_factory=_SPTestClass1)
def test_datetime_limits() -> None:
"""Test limiting datetime values in various ways."""
from efro.util import utc_today, utc_this_hour
@ioprepped
@dataclass
class _TestClass:
tval: Annotated[datetime.datetime, IOAttrs(whole_hours=True)]
# Check whole-hour limit when validating/exporting.
obj = _TestClass(tval=utc_this_hour() + datetime.timedelta(minutes=1))
with pytest.raises(ValueError):
dataclass_validate(obj)
obj.tval = utc_this_hour()
dataclass_validate(obj)
# Check whole-days limit when importing.
out = dataclass_to_dict(obj)
out['tval'][-1] += 1
with pytest.raises(ValueError):
dataclass_from_dict(_TestClass, out)
# Check whole-days limit when validating/exporting.
@ioprepped
@dataclass
class _TestClass2:
tval: Annotated[datetime.datetime, IOAttrs(whole_days=True)]
obj2 = _TestClass2(tval=utc_today() + datetime.timedelta(hours=1))
with pytest.raises(ValueError):
dataclass_validate(obj2)
obj2.tval = utc_today()
dataclass_validate(obj2)
# Check whole-days limit when importing.
out = dataclass_to_dict(obj2)
out['tval'][-1] += 1
with pytest.raises(ValueError):
dataclass_from_dict(_TestClass2, out)
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.
@ioprepped
@dataclass
class _TestClass:
@dataclass
class _TestSubClass:
val1: int = 0
val2: Annotated[int, IOAttrs('v2')] = 0
sub1: _TestSubClass = field(default_factory=_TestSubClass)
sub2: Annotated[_TestSubClass,
IOAttrs('s2')] = field(default_factory=_TestSubClass)
# Now let's lookup various storage paths.
lookup = DataclassFieldLookup(_TestClass)
# Make sure lookups are returning correct storage paths.
assert lookup.path(lambda obj: obj.sub1) == 'sub1'
assert lookup.path(lambda obj: obj.sub1.val1) == 'sub1.val1'
assert lookup.path(lambda obj: obj.sub1.val2) == 'sub1.v2'
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.
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.
with pytest.raises(TypeError):
lookup.path(lambda obj: 1)
with pytest.raises(TypeError):
lookup.path(lambda obj: obj.sub1.val1.real)
def test_nested() -> None:
"""Test nesting dataclasses."""
@ioprepped
@dataclass
class _TestClass:
class _TestEnum(Enum):
VAL1 = 'val1'
VAL2 = 'val2'
@dataclass
class _TestSubClass:
ival: int = 0
subval: _TestSubClass = field(default_factory=_TestSubClass)
enval: _TestEnum = _TestEnum.VAL1
def test_extended_data() -> None:
"""Test IOExtendedData functionality."""
@ioprepped
@dataclass
class _TestClass:
vals: tuple[int, int]
# This data lines up.
indata = {'vals': [0, 0]}
_obj = dataclass_from_dict(_TestClass, indata)
# This data doesn't.
indata = {'vals': [0, 0, 0]}
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.
@ioprepped
@dataclass
class _TestClass2(IOExtendedData):
vals: tuple[int, int]
@classmethod
def will_input(cls, data: dict) -> None:
data['vals'] = data['vals'][:2]
def will_output(self) -> None:
self.vals = (0, 0)
# This data lines up.
indata = {'vals': [0, 0]}
_obj2 = dataclass_from_dict(_TestClass2, indata)
# Now this data will too via our custom input filter.
indata = {'vals': [0, 0, 0]}
_obj2 = dataclass_from_dict(_TestClass2, indata)
# Ok, now test output:
# Does the expected thing.
assert dataclass_to_dict(_TestClass(vals=(1, 2))) == {'vals': [1, 2]}
# Uses our output filter.
assert dataclass_to_dict(_TestClass2(vals=(1, 2))) == {'vals': [0, 0]}