Coverage for muutils\json_serialize\serializable_field.py: 40%

40 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-15 21:53 -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 assert len(_args) == 0, f"unexpected positional arguments: {_args}" 

262 return SerializableField( 

263 default=default, 

264 default_factory=default_factory, 

265 init=init, 

266 repr=repr, 

267 hash=hash, 

268 compare=compare, 

269 metadata=metadata, 

270 kw_only=kw_only, 

271 serialize=serialize, 

272 serialization_fn=serialization_fn, 

273 deserialize_fn=deserialize_fn, 

274 assert_type=assert_type, 

275 custom_typecheck_fn=custom_typecheck_fn, 

276 **kwargs, 

277 )