Coverage for muutils\json_serialize\serializable_field.py: 40%
40 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-09 01:48 -0600
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-09 01:48 -0600
1"""extends `dataclasses.Field` for use with `SerializableDataclass`
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.
7"""
9from __future__ import annotations
11import dataclasses
12import sys
13import types
14from typing import Any, Callable, Optional, Union, overload, TypeVar
17# pylint: disable=bad-mcs-classmethod-argument, too-many-arguments, protected-access
20class SerializableField(dataclasses.Field):
21 """extension of `dataclasses.Field` with additional serialization properties"""
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 )
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")
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 )
80 if metadata is not None:
81 super_kwargs["metadata"] = metadata
82 else:
83 super_kwargs["metadata"] = types.MappingProxyType({})
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"]
92 # actually init the super class
93 super().__init__(**super_kwargs) # type: ignore[call-arg]
95 # now init the new fields
96 self.serialize: bool = serialize
97 self.serialization_fn: Optional[Callable[[Any], Any]] = serialization_fn
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
108 self.assert_type: bool = assert_type
109 self.custom_typecheck_fn: Optional[Callable[[type], bool]] = custom_typecheck_fn
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 )
130Sfield_T = TypeVar("Sfield_T")
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`
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 ```
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.
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:
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 ```
243 using `deserialize_fn` instead:
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 ```
253 In the above code, `my_field` is an int but will be serialized as a string.
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 )