Coverage for muutils/json_serialize/serializable_field.py: 40%
40 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-04 03:33 -0600
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-04 03:33 -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.
231 - `assert_type`: whether to assert the type of the field when loading. if `False`, will not check the type of the field.
232 - `custom_typecheck_fn`: function taking the type of the field and returning whether the type itself is valid. if not provided, will use the default type checking.
234 # Gotchas:
235 - `loading_fn` takes the dict of the **class**, not the field. if you wanted a `loading_fn` that does nothing, you'd write:
237 ```python
238 class MyClass:
239 my_field: int = serializable_field(
240 serialization_fn=lambda x: str(x),
241 loading_fn=lambda x["my_field"]: int(x)
242 )
243 ```
245 using `deserialize_fn` instead:
247 ```python
248 class MyClass:
249 my_field: int = serializable_field(
250 serialization_fn=lambda x: str(x),
251 deserialize_fn=lambda x: int(x)
252 )
253 ```
255 In the above code, `my_field` is an int but will be serialized as a string.
257 note that if not using ZANJ, and you have a class inside a container, you MUST provide
258 `serialization_fn` and `loading_fn` to serialize and load the container.
259 ZANJ will automatically do this for you.
261 # TODO: `custom_value_check_fn`: function taking the value of the field and returning whether the value itself is valid. if not provided, any value is valid as long as it passes the type test
262 """
263 assert len(_args) == 0, f"unexpected positional arguments: {_args}"
264 return SerializableField(
265 default=default,
266 default_factory=default_factory,
267 init=init,
268 repr=repr,
269 hash=hash,
270 compare=compare,
271 metadata=metadata,
272 kw_only=kw_only,
273 serialize=serialize,
274 serialization_fn=serialization_fn,
275 deserialize_fn=deserialize_fn,
276 assert_type=assert_type,
277 custom_typecheck_fn=custom_typecheck_fn,
278 **kwargs,
279 )