/
encoders.py
242 lines (184 loc) · 6.44 KB
/
encoders.py
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
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
try:
from typing import Type # noqa: F401
except ImportError:
pass
import json
import platform
from .data import Data # noqa: F401
from .exceptions import DecoderError
IDictionary = None
numpy_support = False
dotnet_support = False
# We don't do this from `compas.IPY` to avoid circular imports
if "ironpython" == platform.python_implementation().lower():
dotnet_support = True
try:
import System # type: ignore
from System.Collections.Generic import IDictionary # type: ignore
except: # noqa: E722
pass
try:
import numpy as np
numpy_support = True
except (ImportError, SyntaxError):
numpy_support = False
def cls_from_dtype(dtype): # type: (...) -> Type[Data]
"""Get the class object corresponding to a COMPAS data type specification.
Parameters
----------
dtype : str
The data type of the COMPAS object in the following format:
'{}/{}'.format(o.__class__.__module__, o.__class__.__name__).
Returns
-------
:class:`compas.data.Data`
Raises
------
ValueError
If the data type is not in the correct format.
ImportError
If the module can't be imported.
AttributeError
If the module doesn't contain the specified data type.
"""
mod_name, attr_name = dtype.split("/")
module = __import__(mod_name, fromlist=[attr_name])
return getattr(module, attr_name)
class DataEncoder(json.JSONEncoder):
"""Data encoder for custom JSON serialization with support for COMPAS data structures and geometric primitives.
The encoder adds the following conversions to the JSON serialisation process:
* Numpy objects to their Python equivalents;
* iterables to lists; and
* :class:`compas.data.Data` objects,
such as geometric primitives and shapes, data structures, robots, ...,
to a dict with the following structure: ``{'dtype': o.__dtype__, 'data': o.__data__}``
See Also
--------
compas.data.Data
compas.data.DataDecoder
Examples
--------
Explicit use case.
>>> import json
>>> from compas.data import DataEncoder
>>> from compas.geometry import Point
>>> point = Point(0, 0, 0)
>>> with open('point.json', 'w') as f:
... json.dump(point, f, cls=DataEncoder)
...
Implicit use case.
>>> from compas.data import json_dump
>>> from compas.geometry import Point
>>> point = Point(0, 0, 0)
>>> json_dump(point, 'point.json')
"""
minimal = False
def default(self, o):
"""Return an object in serialized form.
Parameters
----------
o : object
The object to serialize.
Returns
-------
str
The serialized object.
"""
if hasattr(o, "__jsondump__"):
return o.__jsondump__(minimal=DataEncoder.minimal)
if hasattr(o, "__next__"):
return list(o)
if numpy_support:
if isinstance(o, np.ndarray):
return o.tolist()
if isinstance(
o,
(
np.int_,
np.intc,
np.intp,
np.int8,
np.int16,
np.int32,
np.int64,
np.uint8,
np.uint16,
np.uint32,
np.uint64,
), # type: ignore
):
return int(o)
if isinstance(o, (np.float_, np.float16, np.float32, np.float64)): # type: ignore
return float(o)
if isinstance(o, np.bool_):
return bool(o)
if isinstance(o, np.void):
return None
if dotnet_support:
if isinstance(o, (System.Decimal, System.Double, System.Single)):
return float(o)
return super(DataEncoder, self).default(o)
class DataDecoder(json.JSONDecoder):
"""Data decoder for custom JSON serialization with support for COMPAS data structures and geometric primitives.
The decoder hooks into the JSON deserialisation process
to reconstruct :class:`compas.data.Data` objects,
such as geometric primitives and shapes, data structures, robots, ...,
from the serialized data when possible.
The reconstruction is possible if
* the serialized data has the following structure: ``{"dtype": "...", 'data': {...}}``;
* a class can be imported into the current scope from the info in ``o["dtype"]``; and
* the imported class has a method ``__from_data__``.
See Also
--------
compas.data.Data
compas.data.DataEncoder
Examples
--------
Explicit use case.
>>> import json
>>> from compas.data import DataDecoder
>>> with open('point.json', 'r') as f:
... point = json.load(f, cls=DataDecoder)
...
Implicit use case.
>>> from compas.data import json_load
>>> point = json_load('point.json')
"""
def __init__(self, *args, **kwargs):
super(DataDecoder, self).__init__(object_hook=self.object_hook, *args, **kwargs)
def object_hook(self, o):
"""Reconstruct a deserialized object.
Parameters
----------
o : object
Returns
-------
object
A (reconstructed), deserialized object.
"""
if "dtype" not in o:
return o
try:
cls = cls_from_dtype(o["dtype"])
except ValueError:
raise DecoderError(
"The data type of the object should be in the following format: '{}/{}'".format(
o.__class__.__module__,
o.__class__.__name__,
)
)
except ImportError:
raise DecoderError("The module of the data type can't be found: {}.".format(o["dtype"]))
except AttributeError:
raise DecoderError("The data type can't be found in the specified module: {}.".format(o["dtype"]))
data = o["data"]
guid = o.get("guid")
name = o.get("name")
# Kick-off __from_data__ from a rebuilt Python dictionary instead of the C# data type
if IDictionary and isinstance(o, IDictionary[str, object]):
data = {key: data[key] for key in data.Keys}
obj = cls.__jsonload__(data, guid=guid, name=name)
return obj