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_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 _hosting_state_response(self, result: Optional[dict[str, Any]]) -> None: # Its possible for this to come back to us after our UI is dead; # ignore in that case. if not self._container: return state: Optional[PrivateHostingState] = None if result is not None: self._debug_server_comm('got private party state response') try: state = dataclass_from_dict(PrivateHostingState, result, discard_unknown_attrs=True) except Exception: ba.print_exception('Got invalid PrivateHostingState data') else: self._debug_server_comm('private party state response errored') # Hmm I guess let's just ignore failed responses?... # Or should we show some sort of error state to the user?... if result is None or state is None: return self._waiting_for_initial_state = False self._waiting_for_start_stop_response = False self._hostingstate = state self._refresh_sub_tab()
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]}
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_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_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 _load_config_from_file(self, print_confirmation: bool) -> ServerConfig: out: Optional[ServerConfig] = None if not os.path.exists(self._config_path): # Special case: # If the user didn't specify a particular config file, allow # gracefully falling back to defaults if the default one is # missing. if not self._user_provided_config_path: if print_confirmation: print( f'{Clr.YLW}Default config file not found' f' (\'{self._config_path}\'); using default' f' settings.{Clr.RST}', flush=True) self._config_mtime = None self._last_config_mtime_check_time = time.time() return ServerConfig() # Don't be so lenient if the user pointed us at one though. raise RuntimeError( f"Config file not found: '{self._config_path}'.") import yaml with open(self._config_path) as infile: user_config_raw = yaml.safe_load(infile.read()) # An empty config file will yield None, and that's ok. if user_config_raw is not None: out = dataclass_from_dict(ServerConfig, user_config_raw) # Update our known mod-time since we know it exists. self._config_mtime = Path(self._config_path).stat().st_mtime self._last_config_mtime_check_time = time.time() # Go with defaults if we weren't able to load anything. if out is None: out = ServerConfig() if print_confirmation: print(f'{Clr.CYN}Valid server config file loaded.{Clr.RST}', flush=True) return out
def _from_dict(self, data: dict, types_by_id: dict[int, type[Any]], opname: str) -> Any: """Decode a message from a json string.""" msgdict: Optional[dict] m_id = data.get('t') # Allow omitting 'm' dict if its empty. msgdict = data.get('m', {}) assert isinstance(m_id, int) assert isinstance(msgdict, dict) # Decode this particular type. msgtype = types_by_id.get(m_id) if msgtype is None: raise UnregisteredMessageIDError( f'Got unregistered {opname} id of {m_id}.') return dataclass_from_dict(msgtype, msgdict)
def _connect_response(self, result: Optional[dict[str, Any]]) -> None: try: self._connect_press_time = None if result is None: raise RuntimeError() cresult = dataclass_from_dict(PrivatePartyConnectResult, result, discard_unknown_attrs=True) if cresult.error is not None: self._debug_server_comm('got error connect response') ba.screenmessage( ba.Lstr(translate=('serverResponses', cresult.error)), (1, 0, 0)) ba.playsound(ba.getsound('error')) return self._debug_server_comm('got valid connect response') assert cresult.addr is not None and cresult.port is not None _ba.connect_to_party(cresult.addr, port=cresult.port) except Exception: self._debug_server_comm('got connect response error') ba.playsound(ba.getsound('error'))
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 _decode(self, data: str, types_by_id: dict[int, type[Any]], opname: str) -> Any: """Decode a message from a json string.""" msgfull = json.loads(data) assert isinstance(msgfull, dict) msgdict: Optional[dict] if self._type_key is not None: m_id = msgfull.pop(self._type_key) msgdict = msgfull assert isinstance(m_id, int) else: m_id = msgfull.get('t') msgdict = msgfull.get('m') assert isinstance(m_id, int) assert isinstance(msgdict, dict) # Decode this particular type. msgtype = types_by_id.get(m_id) if msgtype is None: raise UnregisteredMessageIDError( f'Got unregistered {opname} id of {m_id}.') out = dataclass_from_dict(msgtype, msgdict) # Special case: if we get EmptyResponse, we simply return None. if isinstance(out, EmptyResponse): return None # Special case: a remote error occurred. Raise a local Exception # instead of returning the message. if isinstance(out, ErrorResponse): assert opname == 'response' if (self.preserve_clean_errors and out.error_type is ErrorType.CLEAN): raise CleanError(out.error_message) raise RemoteError(out.error_message) return out
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(TypeError): dataclass_from_dict(_TestClass, {'tupleval': []}) with pytest.raises(TypeError): dataclass_from_dict(_TestClass, {'tupleval': [1, 1, 1]}) with pytest.raises(TypeError): 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_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_soft_default() -> None: """Test soft_default IOAttr value.""" # pylint: disable=too-many-locals # pylint: disable=too-many-statements # Try both of these with and without storage_name to make sure # soft_default interacts correctly with both cases. @ioprepped @dataclass class _TestClassA: ival: int @ioprepped @dataclass class _TestClassA2: ival: Annotated[int, IOAttrs('i')] @ioprepped @dataclass class _TestClassB: ival: Annotated[int, IOAttrs(soft_default=0)] @ioprepped @dataclass class _TestClassB2: ival: Annotated[int, IOAttrs('i', soft_default=0)] @ioprepped @dataclass class _TestClassB3: ival: Annotated[int, IOAttrs('i', soft_default_factory=lambda: 0)] # These should fail because there's no value for ival. with pytest.raises(ValueError): dataclass_from_dict(_TestClassA, {}) with pytest.raises(ValueError): dataclass_from_dict(_TestClassA2, {}) # 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, {}) # soft_default should also allow using store_default=False without # requiring the dataclass to contain a default or default_factory @ioprepped @dataclass class _TestClassC: ival: Annotated[int, IOAttrs(store_default=False)] = 0 assert dataclass_to_dict(_TestClassC()) == {} # This should fail since store_default would be meaningless without # any source for the default value. with pytest.raises(TypeError): @ioprepped @dataclass class _TestClassC2: ival: Annotated[int, IOAttrs(store_default=False)] # However with our shiny soft_default it should work. @ioprepped @dataclass class _TestClassC3: ival: Annotated[int, IOAttrs(store_default=False, soft_default=0)] assert dataclass_to_dict(_TestClassC3(0)) == {} @ioprepped @dataclass class _TestClassC3b: ival: Annotated[ int, IOAttrs(store_default=False, soft_default_factory=lambda: 0)] assert dataclass_to_dict(_TestClassC3b(0)) == {} # We disallow passing a few mutable types as soft_defaults # just as dataclass does with regular defaults. with pytest.raises(TypeError): @ioprepped @dataclass class _TestClassD: lval: Annotated[list, IOAttrs(soft_default=[])] with pytest.raises(TypeError): @ioprepped @dataclass class _TestClassD2: # noinspection PyTypeHints lval: Annotated[set, IOAttrs(soft_default=set())] with pytest.raises(TypeError): @ioprepped @dataclass 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 # early the better) with pytest.raises(TypeError): @ioprepped @dataclass class _TestClassE: lval: Annotated[int, IOAttrs(soft_default='')] with pytest.raises(TypeError): @ioprepped @dataclass class _TestClassE2: lval: Annotated[str, IOAttrs(soft_default=45)] with pytest.raises(TypeError): @ioprepped @dataclass 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). @ioprepped @dataclass class _TestClassE4: lval: Annotated[Optional[str], IOAttrs(soft_default=None)] @ioprepped @dataclass class _TestClassE5: lval: Annotated[Optional[str], 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. @ioprepped @dataclass class _TestClassE6: lval: Annotated[tuple[int, int], IOAttrs(soft_default=('foo', 'bar'))] with pytest.raises(TypeError): dataclass_from_dict(_TestClassE6, {}) @ioprepped @dataclass class _TestClassE7: lval: Annotated[Optional[bool], IOAttrs(soft_default=12)] 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). @ioprepped @dataclass class _TestClassE8: ival: Annotated[int, IOAttrs(soft_default=1, store_default=False)] = 2 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. orig = _TestClassE8(ival=1) todict = dataclass_to_dict(orig) assert todict == {} assert dataclass_from_dict(_TestClassE8, todict) == orig # Instantiate with the dataclass default and it should still get # explicitly despite the store_default=False because soft_default # takes precedence. orig = _TestClassE8() todict = dataclass_to_dict(orig) assert todict == {'ival': 2} assert dataclass_from_dict(_TestClassE8, todict) == orig