-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathdictable.py
More file actions
387 lines (312 loc) · 10.3 KB
/
Copy pathdictable.py
File metadata and controls
387 lines (312 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
import inspect
import json
import logging
import os
import numpy as np
from pathlib import Path
from typing import Union, Callable, Set, Tuple
from autoconf.class_path import get_class_path, get_class
logger = logging.getLogger(__name__)
np_type_map = {
"bool": "bool_",
}
def nd_array_as_dict(obj: np.ndarray) -> dict:
"""
Converts a numpy array to a dictionary representation.
"""
np_type = str(obj.dtype)
return {
"type": "ndarray",
"array": obj.tolist(),
"dtype": np_type_map.get(np_type, np_type),
}
def nd_array_from_dict(nd_array_dict: dict, **_) -> np.ndarray:
"""
Converts a dictionary representation back to a numpy array.
"""
return np.array(nd_array_dict["array"], dtype=getattr(np, nd_array_dict["dtype"]))
def is_array(obj) -> bool:
"""
True if the object is a numpy array or an ArrayImpl (i.e. from JAX)
"""
if isinstance(obj, np.ndarray):
return True
try:
return obj.__class__.__name__ == "ArrayImpl"
except AttributeError:
return False
def compound_key_dict(obj):
"""
Converts a dictionary with compound keys to a dictionary with a single key.
"""
return {
"type": "compound_dict",
"arguments": [
{
"key": to_dict(key),
"value": to_dict(value),
}
for key, value in obj.items()
],
}
def to_dict(obj, filter_args: Tuple[str, ...] = ()) -> dict:
if hasattr(obj, "dict"):
try:
return obj.dict()
except TypeError as e:
logger.debug(e)
if isinstance(obj, (int, float, str, bool, type(None))):
return obj
if isinstance(obj, slice):
return {
"type": "slice",
"start": to_dict(obj.start),
"stop": to_dict(obj.stop),
"step": to_dict(obj.step),
}
if isinstance(obj, np.number):
return {
"type": "np.number",
"dtype": str(obj.dtype),
"value": obj.item(),
}
if inspect.isfunction(obj):
return {
"type": "function",
"class_path": obj.__module__ + "." + obj.__qualname__,
}
if is_array(obj):
try:
return nd_array_as_dict(obj)
except Exception as e:
logger.info(e)
if isinstance(obj, Path):
return {
"type": "path",
"path": str(obj),
}
if inspect.isclass(obj):
return {
"type": "type",
"class_path": get_class_path(obj),
}
if isinstance(obj, list):
return {"type": "list", "values": list(map(to_dict, obj))}
if isinstance(obj, tuple):
return {"type": "tuple", "values": list(map(to_dict, obj))}
if isinstance(obj, dict):
if any(
not (isinstance(key, (str, int, float, bool)) or key is None)
for key in obj.keys()
):
return compound_key_dict(obj)
return {
"type": "dict",
"arguments": {
key: to_dict(value)
for key, value in obj.items()
if key not in filter_args
},
}
if obj.__class__.__name__ == "method":
return to_dict(obj())
if obj.__class__.__module__ == "builtins":
return obj
if inspect.isclass(type(obj)):
return instance_as_dict(obj, filter_args=filter_args)
return obj
def get_arguments(obj) -> Set[str]:
"""
Get the arguments of a class. This is done by inspecting the constructor.
If the constructor has a **kwargs parameter, the arguments of the base classes are also included.
Parameters
----------
obj
The class to get the arguments of.
Returns
-------
A set of the arguments of the class.
"""
args_spec = inspect.getfullargspec(obj.__init__)
args = set(args_spec.args[1:])
if args_spec.varkw:
for base in obj.__bases__:
if base is object:
continue
args |= get_arguments(base)
return args
def instance_as_dict(obj, filter_args: Tuple[str, ...] = ()):
"""
Convert an instance of a class to a dictionary representation.
Serialises any children of the object which are given as constructor arguments
or included in the __identifier_fields__ attribute.
Sets any fields in the __nullify_fields__ attribute to None.
Parameters
----------
obj
The instance of the class to be converted to a dictionary representation.
filter_args
A tuple of arguments to exclude from the dictionary representation.
Returns
-------
A dictionary representation of the instance.
"""
arguments = get_arguments(type(obj))
try:
arguments |= set(obj.__identifier_fields__)
except (AttributeError, TypeError):
pass
argument_dict = {
arg: getattr(obj, arg)
for arg in arguments
if arg not in filter_args
if hasattr(obj, arg)
and not inspect.ismethod(
getattr(obj, arg),
)
}
try:
for field in obj.__nullify_fields__:
argument_dict[field] = None
except (AttributeError, TypeError):
pass
try:
for field in obj.__exclude_fields__:
try:
argument_dict.pop(field)
except KeyError:
logger.debug(f"Field {field} not found in object")
except (AttributeError, TypeError):
pass
return {
"type": "instance",
"class_path": get_class_path(obj.__class__),
"arguments": {key: to_dict(value) for key, value in argument_dict.items()},
}
__parsers = {
"ndarray": nd_array_from_dict,
}
def register_parser(type_: str, parser: Callable[[dict], object]):
"""
Register a parser for a given type.
This parser will be used to instantiate objects of the given type from a
dictionary representation.
Parameters
----------
type_
The type of the object to be parsed. This is a string uniquely
identifying the type.
parser
A function which takes a dictionary representation of an object and
returns an instance of the object.
"""
__parsers[type_] = parser
def from_dict(dictionary, **kwargs):
"""
Instantiate an instance of a class from its dictionary representation.
Parameters
----------
dictionary
An object which may be a dictionary representation of an object.
This may contain the following keys:
type: str
The type of the object. This may be a built-in type, a numpy array,
a list, a dictionary, a class, or an instance of a class.
If a parser has been registered for the given type that parser will
be used to instantiate the object.
class_path: str
The path to the class of the object. This is used to instantiate
the object if it is not a built-in type.
arguments: dict
A dictionary of arguments to pass to the class constructor.
Returns
-------
An object that was represented by the dictionary.
"""
if isinstance(dictionary, (int, float, str, bool, type(None))):
return dictionary
if isinstance(dictionary, list):
return list(map(from_dict, dictionary))
if isinstance(dictionary, tuple):
return tuple(map(from_dict, dictionary))
try:
type_ = dictionary["type"]
except KeyError:
logger.debug("No type field in dictionary")
return dictionary
except TypeError as e:
logger.debug(e)
return None
if type_ == "path":
return Path(dictionary["path"])
if type_ == "slice":
return slice(
from_dict(dictionary["start"]),
from_dict(dictionary["stop"]),
from_dict(dictionary["step"]),
)
if type_ == "np.number":
return getattr(
np,
dictionary["dtype"],
)(dictionary["value"])
if type_ == "function":
return get_class(dictionary["class_path"])
if type_ in __parsers:
return __parsers[type_](dictionary, **kwargs)
if type_ == "list":
return list(map(from_dict, dictionary["values"]))
if type_ == "tuple":
return tuple(map(from_dict, dictionary["values"]))
if type_ == "dict":
return {
key: from_dict(value, **kwargs)
for key, value in dictionary["arguments"].items()
}
if type_ == "compound_dict":
return {
from_dict(item["key"], **kwargs): from_dict(item["value"], **kwargs)
for item in dictionary["arguments"]
}
if type_ == "type":
return get_class(dictionary["class_path"])
cls = get_class(dictionary["class_path"])
if cls is np.ndarray:
return nd_array_from_dict(dictionary)
if hasattr(cls, "from_dict"):
return cls.from_dict(dictionary, **kwargs)
# noinspection PyArgumentList
return cls(
**{
name: from_dict(value, **kwargs)
for name, value in dictionary["arguments"].items()
}
)
def from_json(file_path: str):
"""
Load the dictable object to a .json file, whereby all attributes are converted from the .json file's dictionary
representation to create the instance of the object
A json file of the instance can be created from the .json file via the `output_to_json` method.
Parameters
----------
file_path
The path to the .json file that the dictionary representation of the object is loaded from.
"""
with open(file_path, "r+") as f:
cls_dict = json.load(f)
return from_dict(cls_dict)
def output_to_json(obj, file_path: Union[Path, str]):
"""
Output the dictable object to a .json file, whereby all attributes are converted to a dictionary representation
first.
An instance of the object can be created from the .json file via the `from_json` method.
Parameters
----------
file_path
The path to the .json file that the dictionary representation of the object is written too.
"""
file_path = Path(file_path)
file_dir = Path(*file_path.parts[:-1])
file_dir.mkdir(parents=True, exist_ok=True)
with open(file_path, "w+") as f:
json.dump(to_dict(obj), f, indent=4)