muutils.json_serialize.serializable_field
extends dataclasses.Field for use with SerializableDataclass
In particular, instead of using dataclasses.field, use serializable_field to define fields in a SerializableDataclass.
You provide information on how the field should be serialized and loaded (as well as anything that goes into dataclasses.field)
when you define the field, and the SerializableDataclass will automatically use those functions.
1"""extends `dataclasses.Field` for use with `SerializableDataclass` 2 3In particular, instead of using `dataclasses.field`, use `serializable_field` to define fields in a `SerializableDataclass`. 4You provide information on how the field should be serialized and loaded (as well as anything that goes into `dataclasses.field`) 5when you define the field, and the `SerializableDataclass` will automatically use those functions. 6 7""" 8 9from __future__ import annotations 10 11import dataclasses 12import sys 13import types 14from typing import Any, Callable, Optional, Union, overload, TypeVar 15 16 17# pylint: disable=bad-mcs-classmethod-argument, too-many-arguments, protected-access 18 19 20class SerializableField(dataclasses.Field): 21 """extension of `dataclasses.Field` with additional serialization properties""" 22 23 __slots__ = ( 24 # from dataclasses.Field.__slots__ 25 "name", 26 "type", 27 "default", 28 "default_factory", 29 "repr", 30 "hash", 31 "init", 32 "compare", 33 "metadata", 34 "kw_only", 35 "_field_type", # Private: not to be used by user code. 36 # new ones 37 "serialize", 38 "serialization_fn", 39 "loading_fn", 40 "deserialize_fn", # new alternative to loading_fn 41 "assert_type", 42 "custom_typecheck_fn", 43 ) 44 45 def __init__( 46 self, 47 default: Union[Any, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 48 default_factory: Union[ 49 Callable[[], Any], dataclasses._MISSING_TYPE 50 ] = dataclasses.MISSING, 51 init: bool = True, 52 repr: bool = True, 53 hash: Optional[bool] = None, 54 compare: bool = True, 55 # TODO: add field for custom comparator (such as serializing) 56 metadata: Optional[types.MappingProxyType] = None, 57 kw_only: Union[bool, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 58 serialize: bool = True, 59 serialization_fn: Optional[Callable[[Any], Any]] = None, 60 loading_fn: Optional[Callable[[Any], Any]] = None, 61 deserialize_fn: Optional[Callable[[Any], Any]] = None, 62 assert_type: bool = True, 63 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 64 ): 65 # TODO: should we do this check, or assume the user knows what they are doing? 66 if init and not serialize: 67 raise ValueError("Cannot have init=True and serialize=False") 68 69 # need to assemble kwargs in this hacky way so as not to upset type checking 70 super_kwargs: dict[str, Any] = dict( 71 default=default, 72 default_factory=default_factory, 73 init=init, 74 repr=repr, 75 hash=hash, 76 compare=compare, 77 kw_only=kw_only, 78 ) 79 80 if metadata is not None: 81 super_kwargs["metadata"] = metadata 82 else: 83 super_kwargs["metadata"] = types.MappingProxyType({}) 84 85 # special check, kw_only is not supported in python <3.9 and `dataclasses.MISSING` is truthy 86 if sys.version_info < (3, 10): 87 if super_kwargs["kw_only"] == True: # noqa: E712 88 raise ValueError("kw_only is not supported in python >=3.9") 89 else: 90 del super_kwargs["kw_only"] 91 92 # actually init the super class 93 super().__init__(**super_kwargs) # type: ignore[call-arg] 94 95 # now init the new fields 96 self.serialize: bool = serialize 97 self.serialization_fn: Optional[Callable[[Any], Any]] = serialization_fn 98 99 if loading_fn is not None and deserialize_fn is not None: 100 raise ValueError( 101 "Cannot pass both loading_fn and deserialize_fn, pass only one. ", 102 "`loading_fn` is the older interface and takes the dict of the class, ", 103 "`deserialize_fn` is the new interface and takes only the field's value.", 104 ) 105 self.loading_fn: Optional[Callable[[Any], Any]] = loading_fn 106 self.deserialize_fn: Optional[Callable[[Any], Any]] = deserialize_fn 107 108 self.assert_type: bool = assert_type 109 self.custom_typecheck_fn: Optional[Callable[[type], bool]] = custom_typecheck_fn 110 111 @classmethod 112 def from_Field(cls, field: dataclasses.Field) -> "SerializableField": 113 """copy all values from a `dataclasses.Field` to new `SerializableField`""" 114 return cls( 115 default=field.default, 116 default_factory=field.default_factory, 117 init=field.init, 118 repr=field.repr, 119 hash=field.hash, 120 compare=field.compare, 121 metadata=field.metadata, 122 kw_only=getattr(field, "kw_only", dataclasses.MISSING), # for python <3.9 123 serialize=field.repr, # serialize if it's going to be repr'd 124 serialization_fn=None, 125 loading_fn=None, 126 deserialize_fn=None, 127 ) 128 129 130Sfield_T = TypeVar("Sfield_T") 131 132 133@overload 134def serializable_field( 135 *_args, 136 default_factory: Callable[[], Sfield_T], 137 default: dataclasses._MISSING_TYPE = dataclasses.MISSING, 138 init: bool = True, 139 repr: bool = True, 140 hash: Optional[bool] = None, 141 compare: bool = True, 142 metadata: Optional[types.MappingProxyType] = None, 143 kw_only: Union[bool, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 144 serialize: bool = True, 145 serialization_fn: Optional[Callable[[Any], Any]] = None, 146 deserialize_fn: Optional[Callable[[Any], Any]] = None, 147 assert_type: bool = True, 148 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 149 **kwargs: Any, 150) -> Sfield_T: ... 151@overload 152def serializable_field( 153 *_args, 154 default: Sfield_T, 155 default_factory: dataclasses._MISSING_TYPE = dataclasses.MISSING, 156 init: bool = True, 157 repr: bool = True, 158 hash: Optional[bool] = None, 159 compare: bool = True, 160 metadata: Optional[types.MappingProxyType] = None, 161 kw_only: Union[bool, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 162 serialize: bool = True, 163 serialization_fn: Optional[Callable[[Any], Any]] = None, 164 deserialize_fn: Optional[Callable[[Any], Any]] = None, 165 assert_type: bool = True, 166 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 167 **kwargs: Any, 168) -> Sfield_T: ... 169@overload 170def serializable_field( 171 *_args, 172 default: dataclasses._MISSING_TYPE = dataclasses.MISSING, 173 default_factory: dataclasses._MISSING_TYPE = dataclasses.MISSING, 174 init: bool = True, 175 repr: bool = True, 176 hash: Optional[bool] = None, 177 compare: bool = True, 178 metadata: Optional[types.MappingProxyType] = None, 179 kw_only: Union[bool, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 180 serialize: bool = True, 181 serialization_fn: Optional[Callable[[Any], Any]] = None, 182 deserialize_fn: Optional[Callable[[Any], Any]] = None, 183 assert_type: bool = True, 184 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 185 **kwargs: Any, 186) -> Any: ... 187def serializable_field( 188 *_args, 189 default: Union[Any, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 190 default_factory: Union[Any, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 191 init: bool = True, 192 repr: bool = True, 193 hash: Optional[bool] = None, 194 compare: bool = True, 195 metadata: Optional[types.MappingProxyType] = None, 196 kw_only: Union[bool, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 197 serialize: bool = True, 198 serialization_fn: Optional[Callable[[Any], Any]] = None, 199 deserialize_fn: Optional[Callable[[Any], Any]] = None, 200 assert_type: bool = True, 201 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 202 **kwargs: Any, 203) -> Any: 204 """Create a new `SerializableField` 205 206 ``` 207 default: Sfield_T | dataclasses._MISSING_TYPE = dataclasses.MISSING, 208 default_factory: Callable[[], Sfield_T] 209 | dataclasses._MISSING_TYPE = dataclasses.MISSING, 210 init: bool = True, 211 repr: bool = True, 212 hash: Optional[bool] = None, 213 compare: bool = True, 214 metadata: types.MappingProxyType | None = None, 215 kw_only: bool | dataclasses._MISSING_TYPE = dataclasses.MISSING, 216 # ---------------------------------------------------------------------- 217 # new in `SerializableField`, not in `dataclasses.Field` 218 serialize: bool = True, 219 serialization_fn: Optional[Callable[[Any], Any]] = None, 220 loading_fn: Optional[Callable[[Any], Any]] = None, 221 deserialize_fn: Optional[Callable[[Any], Any]] = None, 222 assert_type: bool = True, 223 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 224 ``` 225 226 # new Parameters: 227 - `serialize`: whether to serialize this field when serializing the class' 228 - `serialization_fn`: function taking the instance of the field and returning a serializable object. If not provided, will iterate through the `SerializerHandler`s defined in `muutils.json_serialize.json_serialize` 229 - `loading_fn`: function taking the serialized object and returning the instance of the field. If not provided, will take object as-is. 230 - `deserialize_fn`: new alternative to `loading_fn`. takes only the field's value, not the whole class. if both `loading_fn` and `deserialize_fn` are provided, an error will be raised. 231 232 # Gotchas: 233 - `loading_fn` takes the dict of the **class**, not the field. if you wanted a `loading_fn` that does nothing, you'd write: 234 235 ```python 236 class MyClass: 237 my_field: int = serializable_field( 238 serialization_fn=lambda x: str(x), 239 loading_fn=lambda x["my_field"]: int(x) 240 ) 241 ``` 242 243 using `deserialize_fn` instead: 244 245 ```python 246 class MyClass: 247 my_field: int = serializable_field( 248 serialization_fn=lambda x: str(x), 249 deserialize_fn=lambda x: int(x) 250 ) 251 ``` 252 253 In the above code, `my_field` is an int but will be serialized as a string. 254 255 note that if not using ZANJ, and you have a class inside a container, you MUST provide 256 `serialization_fn` and `loading_fn` to serialize and load the container. 257 ZANJ will automatically do this for you. 258 """ 259 assert len(_args) == 0, f"unexpected positional arguments: {_args}" 260 return SerializableField( 261 default=default, 262 default_factory=default_factory, 263 init=init, 264 repr=repr, 265 hash=hash, 266 compare=compare, 267 metadata=metadata, 268 kw_only=kw_only, 269 serialize=serialize, 270 serialization_fn=serialization_fn, 271 deserialize_fn=deserialize_fn, 272 assert_type=assert_type, 273 custom_typecheck_fn=custom_typecheck_fn, 274 **kwargs, 275 )
21class SerializableField(dataclasses.Field): 22 """extension of `dataclasses.Field` with additional serialization properties""" 23 24 __slots__ = ( 25 # from dataclasses.Field.__slots__ 26 "name", 27 "type", 28 "default", 29 "default_factory", 30 "repr", 31 "hash", 32 "init", 33 "compare", 34 "metadata", 35 "kw_only", 36 "_field_type", # Private: not to be used by user code. 37 # new ones 38 "serialize", 39 "serialization_fn", 40 "loading_fn", 41 "deserialize_fn", # new alternative to loading_fn 42 "assert_type", 43 "custom_typecheck_fn", 44 ) 45 46 def __init__( 47 self, 48 default: Union[Any, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 49 default_factory: Union[ 50 Callable[[], Any], dataclasses._MISSING_TYPE 51 ] = dataclasses.MISSING, 52 init: bool = True, 53 repr: bool = True, 54 hash: Optional[bool] = None, 55 compare: bool = True, 56 # TODO: add field for custom comparator (such as serializing) 57 metadata: Optional[types.MappingProxyType] = None, 58 kw_only: Union[bool, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 59 serialize: bool = True, 60 serialization_fn: Optional[Callable[[Any], Any]] = None, 61 loading_fn: Optional[Callable[[Any], Any]] = None, 62 deserialize_fn: Optional[Callable[[Any], Any]] = None, 63 assert_type: bool = True, 64 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 65 ): 66 # TODO: should we do this check, or assume the user knows what they are doing? 67 if init and not serialize: 68 raise ValueError("Cannot have init=True and serialize=False") 69 70 # need to assemble kwargs in this hacky way so as not to upset type checking 71 super_kwargs: dict[str, Any] = dict( 72 default=default, 73 default_factory=default_factory, 74 init=init, 75 repr=repr, 76 hash=hash, 77 compare=compare, 78 kw_only=kw_only, 79 ) 80 81 if metadata is not None: 82 super_kwargs["metadata"] = metadata 83 else: 84 super_kwargs["metadata"] = types.MappingProxyType({}) 85 86 # special check, kw_only is not supported in python <3.9 and `dataclasses.MISSING` is truthy 87 if sys.version_info < (3, 10): 88 if super_kwargs["kw_only"] == True: # noqa: E712 89 raise ValueError("kw_only is not supported in python >=3.9") 90 else: 91 del super_kwargs["kw_only"] 92 93 # actually init the super class 94 super().__init__(**super_kwargs) # type: ignore[call-arg] 95 96 # now init the new fields 97 self.serialize: bool = serialize 98 self.serialization_fn: Optional[Callable[[Any], Any]] = serialization_fn 99 100 if loading_fn is not None and deserialize_fn is not None: 101 raise ValueError( 102 "Cannot pass both loading_fn and deserialize_fn, pass only one. ", 103 "`loading_fn` is the older interface and takes the dict of the class, ", 104 "`deserialize_fn` is the new interface and takes only the field's value.", 105 ) 106 self.loading_fn: Optional[Callable[[Any], Any]] = loading_fn 107 self.deserialize_fn: Optional[Callable[[Any], Any]] = deserialize_fn 108 109 self.assert_type: bool = assert_type 110 self.custom_typecheck_fn: Optional[Callable[[type], bool]] = custom_typecheck_fn 111 112 @classmethod 113 def from_Field(cls, field: dataclasses.Field) -> "SerializableField": 114 """copy all values from a `dataclasses.Field` to new `SerializableField`""" 115 return cls( 116 default=field.default, 117 default_factory=field.default_factory, 118 init=field.init, 119 repr=field.repr, 120 hash=field.hash, 121 compare=field.compare, 122 metadata=field.metadata, 123 kw_only=getattr(field, "kw_only", dataclasses.MISSING), # for python <3.9 124 serialize=field.repr, # serialize if it's going to be repr'd 125 serialization_fn=None, 126 loading_fn=None, 127 deserialize_fn=None, 128 )
extension of dataclasses.Field with additional serialization properties
46 def __init__( 47 self, 48 default: Union[Any, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 49 default_factory: Union[ 50 Callable[[], Any], dataclasses._MISSING_TYPE 51 ] = dataclasses.MISSING, 52 init: bool = True, 53 repr: bool = True, 54 hash: Optional[bool] = None, 55 compare: bool = True, 56 # TODO: add field for custom comparator (such as serializing) 57 metadata: Optional[types.MappingProxyType] = None, 58 kw_only: Union[bool, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 59 serialize: bool = True, 60 serialization_fn: Optional[Callable[[Any], Any]] = None, 61 loading_fn: Optional[Callable[[Any], Any]] = None, 62 deserialize_fn: Optional[Callable[[Any], Any]] = None, 63 assert_type: bool = True, 64 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 65 ): 66 # TODO: should we do this check, or assume the user knows what they are doing? 67 if init and not serialize: 68 raise ValueError("Cannot have init=True and serialize=False") 69 70 # need to assemble kwargs in this hacky way so as not to upset type checking 71 super_kwargs: dict[str, Any] = dict( 72 default=default, 73 default_factory=default_factory, 74 init=init, 75 repr=repr, 76 hash=hash, 77 compare=compare, 78 kw_only=kw_only, 79 ) 80 81 if metadata is not None: 82 super_kwargs["metadata"] = metadata 83 else: 84 super_kwargs["metadata"] = types.MappingProxyType({}) 85 86 # special check, kw_only is not supported in python <3.9 and `dataclasses.MISSING` is truthy 87 if sys.version_info < (3, 10): 88 if super_kwargs["kw_only"] == True: # noqa: E712 89 raise ValueError("kw_only is not supported in python >=3.9") 90 else: 91 del super_kwargs["kw_only"] 92 93 # actually init the super class 94 super().__init__(**super_kwargs) # type: ignore[call-arg] 95 96 # now init the new fields 97 self.serialize: bool = serialize 98 self.serialization_fn: Optional[Callable[[Any], Any]] = serialization_fn 99 100 if loading_fn is not None and deserialize_fn is not None: 101 raise ValueError( 102 "Cannot pass both loading_fn and deserialize_fn, pass only one. ", 103 "`loading_fn` is the older interface and takes the dict of the class, ", 104 "`deserialize_fn` is the new interface and takes only the field's value.", 105 ) 106 self.loading_fn: Optional[Callable[[Any], Any]] = loading_fn 107 self.deserialize_fn: Optional[Callable[[Any], Any]] = deserialize_fn 108 109 self.assert_type: bool = assert_type 110 self.custom_typecheck_fn: Optional[Callable[[type], bool]] = custom_typecheck_fn
112 @classmethod 113 def from_Field(cls, field: dataclasses.Field) -> "SerializableField": 114 """copy all values from a `dataclasses.Field` to new `SerializableField`""" 115 return cls( 116 default=field.default, 117 default_factory=field.default_factory, 118 init=field.init, 119 repr=field.repr, 120 hash=field.hash, 121 compare=field.compare, 122 metadata=field.metadata, 123 kw_only=getattr(field, "kw_only", dataclasses.MISSING), # for python <3.9 124 serialize=field.repr, # serialize if it's going to be repr'd 125 serialization_fn=None, 126 loading_fn=None, 127 deserialize_fn=None, 128 )
copy all values from a dataclasses.Field to new SerializableField
188def serializable_field( 189 *_args, 190 default: Union[Any, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 191 default_factory: Union[Any, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 192 init: bool = True, 193 repr: bool = True, 194 hash: Optional[bool] = None, 195 compare: bool = True, 196 metadata: Optional[types.MappingProxyType] = None, 197 kw_only: Union[bool, dataclasses._MISSING_TYPE] = dataclasses.MISSING, 198 serialize: bool = True, 199 serialization_fn: Optional[Callable[[Any], Any]] = None, 200 deserialize_fn: Optional[Callable[[Any], Any]] = None, 201 assert_type: bool = True, 202 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 203 **kwargs: Any, 204) -> Any: 205 """Create a new `SerializableField` 206 207 ``` 208 default: Sfield_T | dataclasses._MISSING_TYPE = dataclasses.MISSING, 209 default_factory: Callable[[], Sfield_T] 210 | dataclasses._MISSING_TYPE = dataclasses.MISSING, 211 init: bool = True, 212 repr: bool = True, 213 hash: Optional[bool] = None, 214 compare: bool = True, 215 metadata: types.MappingProxyType | None = None, 216 kw_only: bool | dataclasses._MISSING_TYPE = dataclasses.MISSING, 217 # ---------------------------------------------------------------------- 218 # new in `SerializableField`, not in `dataclasses.Field` 219 serialize: bool = True, 220 serialization_fn: Optional[Callable[[Any], Any]] = None, 221 loading_fn: Optional[Callable[[Any], Any]] = None, 222 deserialize_fn: Optional[Callable[[Any], Any]] = None, 223 assert_type: bool = True, 224 custom_typecheck_fn: Optional[Callable[[type], bool]] = None, 225 ``` 226 227 # new Parameters: 228 - `serialize`: whether to serialize this field when serializing the class' 229 - `serialization_fn`: function taking the instance of the field and returning a serializable object. If not provided, will iterate through the `SerializerHandler`s defined in `muutils.json_serialize.json_serialize` 230 - `loading_fn`: function taking the serialized object and returning the instance of the field. If not provided, will take object as-is. 231 - `deserialize_fn`: new alternative to `loading_fn`. takes only the field's value, not the whole class. if both `loading_fn` and `deserialize_fn` are provided, an error will be raised. 232 233 # Gotchas: 234 - `loading_fn` takes the dict of the **class**, not the field. if you wanted a `loading_fn` that does nothing, you'd write: 235 236 ```python 237 class MyClass: 238 my_field: int = serializable_field( 239 serialization_fn=lambda x: str(x), 240 loading_fn=lambda x["my_field"]: int(x) 241 ) 242 ``` 243 244 using `deserialize_fn` instead: 245 246 ```python 247 class MyClass: 248 my_field: int = serializable_field( 249 serialization_fn=lambda x: str(x), 250 deserialize_fn=lambda x: int(x) 251 ) 252 ``` 253 254 In the above code, `my_field` is an int but will be serialized as a string. 255 256 note that if not using ZANJ, and you have a class inside a container, you MUST provide 257 `serialization_fn` and `loading_fn` to serialize and load the container. 258 ZANJ will automatically do this for you. 259 """ 260 assert len(_args) == 0, f"unexpected positional arguments: {_args}" 261 return SerializableField( 262 default=default, 263 default_factory=default_factory, 264 init=init, 265 repr=repr, 266 hash=hash, 267 compare=compare, 268 metadata=metadata, 269 kw_only=kw_only, 270 serialize=serialize, 271 serialization_fn=serialization_fn, 272 deserialize_fn=deserialize_fn, 273 assert_type=assert_type, 274 custom_typecheck_fn=custom_typecheck_fn, 275 **kwargs, 276 )
Create a new SerializableField
default: Sfield_T | dataclasses._MISSING_TYPE = dataclasses.MISSING,
default_factory: Callable[[], Sfield_T]
| dataclasses._MISSING_TYPE = dataclasses.MISSING,
init: bool = True,
repr: bool = True,
hash: Optional[bool] = None,
compare: bool = True,
metadata: types.MappingProxyType | None = None,
kw_only: bool | dataclasses._MISSING_TYPE = dataclasses.MISSING,
# ----------------------------------------------------------------------
# new in `SerializableField`, not in `dataclasses.Field`
serialize: bool = True,
serialization_fn: Optional[Callable[[Any], Any]] = None,
loading_fn: Optional[Callable[[Any], Any]] = None,
deserialize_fn: Optional[Callable[[Any], Any]] = None,
assert_type: bool = True,
custom_typecheck_fn: Optional[Callable[[type], bool]] = None,
new Parameters:
serialize: whether to serialize this field when serializing the class'serialization_fn: function taking the instance of the field and returning a serializable object. If not provided, will iterate through theSerializerHandlers defined inmuutils.json_serialize.json_serializeloading_fn: function taking the serialized object and returning the instance of the field. If not provided, will take object as-is.deserialize_fn: new alternative toloading_fn. takes only the field's value, not the whole class. if bothloading_fnanddeserialize_fnare provided, an error will be raised.
Gotchas:
loading_fntakes the dict of the class, not the field. if you wanted aloading_fnthat does nothing, you'd write:
class MyClass:
my_field: int = serializable_field(
serialization_fn=lambda x: str(x),
loading_fn=lambda x["my_field"]: int(x)
)
using deserialize_fn instead:
class MyClass:
my_field: int = serializable_field(
serialization_fn=lambda x: str(x),
deserialize_fn=lambda x: int(x)
)
In the above code, my_field is an int but will be serialized as a string.
note that if not using ZANJ, and you have a class inside a container, you MUST provide
serialization_fn and loading_fn to serialize and load the container.
ZANJ will automatically do this for you.