Skip to content

Commit 86f3912

Browse files
authored
Merge branch 'main' into fix-doctest
2 parents dc18110 + fa49860 commit 86f3912

File tree

4 files changed

+219
-7
lines changed

4 files changed

+219
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* Added instructions for creating new data types to the dev guide.
13+
1214
### Changed
1315

1416
* Fixed `RuntimeError` when using `compas_rhino.unload_modules` in CPython`.
1517
* Fixed bug in `Box.scaled` causing a `TypeError` due to incorrect parameter forwarding.
1618
* Changed argument names of `Box.scale()` to `x`, `y`, `z`, instead of `factor` and made `y` and `z` optional to keep positional arguments backwards compatible.
1719
* Fixed import errors in `compas_rhino.conduits` for Rhino 8.
1820
* Fixed doctest failures.
21+
* Fixed bug in serialization when `compas.datastructures.attributes.AttributeView` is used.
1922

2023
### Removed
2124

@@ -27,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2730
### Changed
2831

2932
* Fixed support for `compas_gpython` in Rhino 8 Grasshopper CPython components.
33+
* Changed installation instructions for Rhino 8 in the user guide.
3034
* Fixed `Graph.from_edges` always returning `None`.
3135

3236
### Removed

docs/devguide/dtypes.rst

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,184 @@
11
Implementing a New Data Type
22
============================
3+
4+
COMPAS data types are classes that are based on :class:`compas.data.Data`.
5+
6+
Data types can be serialized to JSON with
7+
8+
* :func:`compas.json_dump`
9+
* :func:`compas.json_dumps`
10+
* :func:`compas.json_dumpz`
11+
12+
and deserialized with the corresponding "load" functions
13+
14+
* :func:`compas.json_load`
15+
* :func:`compas.json_loads`
16+
* :func:`compas.json_loadz`
17+
18+
All geometry objects and data structures,
19+
and also, for example, the visualization scene,
20+
are serializable data types.
21+
22+
23+
Creating a new data type
24+
========================
25+
26+
In most cases, it is sufficient to implement the ``__data__`` property when creating your custom `Data` class.
27+
28+
.. code-block:: python
29+
30+
class SomeThing(Data):
31+
32+
def __init__(self, a, b)
33+
super().__init__()
34+
# note that if the code needs to be compatible with IronPython
35+
# you should write the following:
36+
# super(SomeThing, self).__init__()
37+
self.a = a
38+
self.b = b
39+
40+
@property
41+
def __data__(self):
42+
return {
43+
"a": self.a,
44+
"b": self.b,
45+
}
46+
47+
48+
>>> custom = SomeThing(a=1, b=2)
49+
>>> compas.json_dump(custom, "custom.json")
50+
>>> result = compas.json_load("custom.json")
51+
>>> isinstance(result, SomeThing)
52+
True
53+
>>> result.a
54+
1
55+
>>> result.b
56+
2
57+
58+
If the attributes stored in the data dictionary defined by the ``__data__`` property
59+
are different from the initialization parameters of the class,
60+
you must also customize the ``__from_data__`` class method to compensate for the difference.
61+
62+
.. code-block:: python
63+
64+
class SomeThing(Data):
65+
66+
def __init__(self)
67+
super().__init__()
68+
# note that if the code needs to be compatible with IronPython
69+
# you should write the following:
70+
# super(SomeThing, self).__init__()
71+
self.items = []
72+
73+
@property
74+
def __data__(self):
75+
return {
76+
"items": self.items,
77+
}
78+
79+
@classmethod
80+
def __from_data__(cls, data):
81+
custom = cls()
82+
for item in data['items']:
83+
custom.add(item)
84+
return custom
85+
86+
def add(self, item):
87+
self.items.append(item)
88+
89+
90+
>>> custom = SomeThing()
91+
>>> custom.add(1)
92+
>>> custom.add(2)
93+
>>> compas.json_dump(custom, "custom.json")
94+
>>> result = compas.json_load("custom.json")
95+
>>> isinstance(result, SomeThing)
96+
True
97+
>>> result.items
98+
[1, 2]
99+
100+
101+
Attribute types
102+
===============
103+
104+
Any attribute that is an instance of a Python base type or a serializable COMPAS data object
105+
can be included in the data dict created by the ``__data__`` property without further processing.
106+
The serialization process will recursively serialize all these attributes.
107+
108+
.. code-block:: python
109+
110+
class SomeThing(Data):
111+
112+
def __init__(self, point, frame, mesh):
113+
super().__init__()
114+
# note that if the code needs to be compatible with IronPython
115+
# you should write the following:
116+
# super(SomeThing, self).__init__()
117+
self.point = point
118+
self.frame = frame
119+
self.mesh = mesh
120+
121+
@property
122+
def __data__(self):
123+
return {
124+
"point": self.point,
125+
"frame": self.frame,
126+
"mesh": self.mesh,
127+
}
128+
129+
130+
>>> import compas
131+
>>> from compas.geometry import Point, Frame
132+
>>> from compas.datastructures import Mesh
133+
>>> point = Point(1, 2, 3)
134+
>>> frame = Frame()
135+
>>> mesh = Mesh.from_meshgrid(10, 10)
136+
>>> custom = SomeThing(point, frame, mesh)
137+
>>> compas.json_dump(custom, "custom.json")
138+
>>> result = compas.json_load("custom.json")
139+
>>> isinstance(result.point, Point)
140+
True
141+
>>> isinstance(result.frame, Frame)
142+
True
143+
>>> isinstance(result.mesh, Mesh)
144+
True
145+
>>> result.point == point
146+
True
147+
>>> result.point is point
148+
False
149+
150+
151+
Note that the the automatic serialization process will incur overhead information
152+
that increases the size of the resulting JSON file.
153+
The performance impact may be significant when many of these instances are serialized.
154+
155+
To avoid this, anticipated conversions can be included explicitly in `__data__` and `__from_data__`.
156+
157+
.. code-block:: python
158+
159+
class SomeThing(Data):
160+
161+
def __init__(self, point, frame, mesh):
162+
super().__init__()
163+
# note that if the code needs to be compatible with IronPython
164+
# you should write the following:
165+
# super(SomeThing, self).__init__()
166+
self.point = point
167+
self.frame = frame
168+
self.mesh = mesh
169+
170+
@property
171+
def __data__(self):
172+
return {
173+
"point": self.point.__data__,
174+
"frame": self.frame.__data__,
175+
"mesh": self.mesh.__data__,
176+
}
177+
178+
@classmethod
179+
def __from_data__(cls, data):
180+
return cls(
181+
Point.__from_data__(data['point']),
182+
Frame.__from_data__(data['frame']),
183+
Mesh.__from_data__(data['mesh']),
184+
)

src/compas/data/encoders.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def default(self, o):
115115
The serialized object.
116116
117117
"""
118+
from compas.datastructures.attributes import AttributeView
118119

119120
if hasattr(o, "__jsondump__"):
120121
return o.__jsondump__(minimal=DataEncoder.minimal)
@@ -153,6 +154,9 @@ def default(self, o):
153154
if isinstance(o, (System.Decimal, System.Double, System.Single)):
154155
return float(o)
155156

157+
if isinstance(o, AttributeView):
158+
return dict(o)
159+
156160
return super(DataEncoder, self).default(o)
157161

158162

tests/compas/data/test_json.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,23 @@ def test_json_native():
2020

2121
def test_json_primitive():
2222
before = Point(0, 0, 0)
23-
after = compas.json_loads(compas.json_dumps(before))
23+
after = compas.json_loads(compas.json_dumps(before)) # type: Point
2424
assert before.__dtype__ == after.__dtype__
2525
assert all(a == b for a, b in zip(before, after))
2626
assert before.guid == after.guid
2727

2828

2929
def test_json_shape():
3030
before = Box(frame=Frame(Point(0, 0, 0), Vector(1, 0, 0), Vector(0, 1, 0)), xsize=1, ysize=1, zsize=1)
31-
after = compas.json_loads(compas.json_dumps(before))
31+
after = compas.json_loads(compas.json_dumps(before)) # type: Box
3232
assert before.__dtype__ == after.__dtype__
3333
assert all(a == b for a, b in zip(before.to_vertices_and_faces()[0], after.to_vertices_and_faces()[0]))
3434
assert before.guid == after.guid
3535

3636

3737
def test_json_xform():
3838
before = Transformation.from_frame_to_frame(Frame.worldXY(), Frame.worldXY())
39-
after = compas.json_loads(compas.json_dumps(before))
39+
after = compas.json_loads(compas.json_dumps(before)) # type: Transformation
4040
assert before.__dtype__ == after.__dtype__
4141
assert all(a == b for a, b in zip(before, after))
4242
assert before.guid == after.guid
@@ -47,7 +47,7 @@ def test_json_graph():
4747
a = before.add_node()
4848
b = before.add_node()
4949
before.add_edge(a, b)
50-
after = compas.json_loads(compas.json_dumps(before))
50+
after = compas.json_loads(compas.json_dumps(before)) # type: Graph
5151
assert before.__dtype__ == after.__dtype__
5252
# assert before.attributes == after.attributes
5353
assert all(before.has_node(node) for node in after.nodes())
@@ -59,7 +59,7 @@ def test_json_graph():
5959

6060
def test_json_mesh():
6161
before = Mesh.from_vertices_and_faces([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], [[0, 1, 2, 3]])
62-
after = compas.json_loads(compas.json_dumps(before))
62+
after = compas.json_loads(compas.json_dumps(before)) # type: Mesh
6363
assert before.__dtype__ == after.__dtype__
6464
# assert before.attributes == after.attributes
6565
assert all(before.has_vertex(vertex) for vertex in after.vertices())
@@ -95,7 +95,7 @@ def test_json_volmesh():
9595
]
9696
],
9797
)
98-
after = compas.json_loads(compas.json_dumps(before))
98+
after = compas.json_loads(compas.json_dumps(before)) # type: VolMesh
9999
assert before.__dtype__ == after.__dtype__
100100
# assert before.attributes == after.attributes
101101
assert all(before.has_vertex(vertex) for vertex in after.vertices())
@@ -121,12 +121,34 @@ def test_json_zip():
121121

122122
before = Box(frame=Frame(Point(0, 0, 0), Vector(1, 0, 0), Vector(0, 1, 0)), xsize=2, ysize=5, zsize=3)
123123
compas.json_dumpz(before, zipfile_name)
124-
after = compas.json_loadz(zipfile_name)
124+
after = compas.json_loadz(zipfile_name) # type: Box
125125
assert before.__dtype__ == after.__dtype__
126126
assert all(a == b for a, b in zip(before.to_vertices_and_faces()[0], after.to_vertices_and_faces()[0]))
127127
assert before.guid == after.guid
128128

129129

130+
def test_json_attributeview():
131+
mesh = Mesh.from_meshgrid(10, 10)
132+
before = Mesh()
133+
for vertex in mesh.vertices():
134+
attr = mesh.vertex_attributes(vertex)
135+
before.add_vertex(key=vertex, attr_dict=attr)
136+
for face in mesh.faces():
137+
attr = mesh.face_attributes(face)
138+
before.add_face(vertices=mesh.face_vertices(face), fkey=face, attr_dict=attr)
139+
after = compas.json_loads(compas.json_dumps(before)) # type: Mesh
140+
141+
assert before.__dtype__ == after.__dtype__
142+
assert all(before.has_vertex(vertex) for vertex in after.vertices())
143+
assert all(after.has_vertex(vertex) for vertex in before.vertices())
144+
assert all(before.has_face(face) for face in after.faces())
145+
assert all(after.has_face(face) for face in before.faces())
146+
assert all(before.has_edge(edge) for edge in after.edges())
147+
assert all(after.has_edge(edge) for edge in before.edges())
148+
assert all(before.face_vertices(a) == after.face_vertices(b) for a, b in zip(before.faces(), after.faces()))
149+
assert before.guid == after.guid
150+
151+
130152
# temporarily commented because folder does not exist yet on main
131153
# def test_json_url():
132154
# data = compas.json_load('https://raw.githubusercontent.com/compas-dev/compas/main/src/compas/data/schemas/graph.json')

0 commit comments

Comments
 (0)