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 theSerializerHandler
s defined inmuutils.json_serialize.json_serialize
loading_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_fn
anddeserialize_fn
are provided, an error will be raised.
Gotchas:
loading_fn
takes the dict of the class, not the field. if you wanted aloading_fn
that 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.