1"""extends `dataclasses.Field` for use with `SerializableDataclass`

2

3 In particular, instead of using `dataclasses.field`, use `serializable_field` to define fields in a `SerializableDataclass`.

4 You provide information on how the field should be serialized and loaded (as well as anything that goes into `dataclasses.field`)

5 when you define the field, and the `SerializableDataclass` will automatically use those functions.

6

7"""

8

9 from __future__ import annotations

10

11 import dataclasses

12 import sys

13 import types

14 from typing import Any, Callable, Optional, Union, overload, TypeVar

15

16

17# pylint: disable=bad-mcs-classmethod-argument, too-many-arguments, protected-access

18

19

20 class 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

130 Sfield_T = TypeVar("Sfield_T")

131

132

133@overload

134 def 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

152 def 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

170 def 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: ...

187 def 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 - `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.

233

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:

236

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 ```

244

245 using `deserialize_fn` instead:

246

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 ```

254

255 In the above code, `my_field` is an int but will be serialized as a string.

256

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.

260

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 )