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

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