Skip to content
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Added `remap_values` to `compas_utilities`.
* Added `compas.datastructures.mesh_slice_plane`.
* Added `compas.json_dump`, `compas.json_dumps`, `compas.json_load`, `compas.json_loads`.

### Changed

* Fixed bug in `compas.datastructures.Network.delete_node`.
* Fixed bug in `compas.datastructures.Network.delete_edge`.
* Fixed bug in select functions for individual objects in `compas_rhino.utilities`.
* Fixed bug in `compas.datastructures.mesh_merge_faces`.
* changed base of `compas.geometry.Transformation` to `compas.base.Base`.

### Removed

Expand Down
Empty file removed ext/PLACEHOLDER
Empty file.
Empty file removed modules/PLACEHOLDER
Empty file.
7 changes: 6 additions & 1 deletion src/compas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import decimal

import compas._os
from compas._json import json_dump, json_dumps, json_load, json_loads


__author__ = 'Tom Van Mele and many others (see AUTHORS.md)'
Expand Down Expand Up @@ -72,7 +73,11 @@
pass


__all__ = ['WINDOWS', 'LINUX', 'MONO', 'IPY', 'RHINO', 'BLENDER', 'set_precision', 'get']
__all__ = [
'WINDOWS', 'LINUX', 'MONO', 'IPY', 'RHINO', 'BLENDER',
'set_precision',
'get',
'json_dump', 'json_load', 'json_dumps', 'json_loads']


def is_windows():
Expand Down
137 changes: 137 additions & 0 deletions src/compas/_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

import json
from compas.utilities import DataEncoder
from compas.utilities import DataDecoder


__all__ = [
'json_dump',
'json_dumps',
'json_load',
'json_loads'
]


def json_dump(data, fp):
"""Write a collection of COMPAS object data to a JSON file.
Parameters
----------
data : any
Any JSON serializable object.
This includes any (combination of) COMPAS object(s).
fp : file-like object or path
A writeable file-like object or the path to a file.
Returns
-------
None
Examples
--------
>>> import compas
>>> from compas.geometry import Point, Vector
>>> data1 = [Point(0, 0, 0), Vector(0, 0, 0)]
>>> compas.json_dump(data1, 'data.json')
>>> data2 = compas.json_load('data.json')
>>> data1 == data2
True
"""
if hasattr(fp, 'write'):
return json.dump(data, fp, cls=DataEncoder)
with open(fp, 'w') as fp:
Comment on lines +43 to +45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the plan is to fix up all of the file readers so that they take file-like objects and paths, I think these lines of code will be used quite a few times. Maybe there should be a utility function which does this. Some sort of context manager like

class open_file(object):
    def __init__(self, fp, *args, **kwargs):
        if hasattr(fp, 'write'):
            self.f = fp
        else:
            self.f = open(fp, *args, **kwargs)

    def __enter__(self):
        return self.f

    def __close__(self):
        self.f.close()
        # but maybe you don't always want to close file-like objects...

And I suppose this could be generalized to handle reading as well.

return json.dump(data, fp, cls=DataEncoder)


def json_dumps(data):
"""Write a collection of COMPAS objects to a JSON string.
Parameters
----------
data : any
Any JSON serializable object.
This includes any (combination of) COMPAS object(s).
Returns
-------
str
Examples
--------
>>> import compas
>>> from compas.geometry import Point, Vector
>>> data1 = [Point(0, 0, 0), Vector(0, 0, 0)]
>>> s = compas.json_dumps(data1)
>>> data2 compas.json_loads(s)
>>> data1 == data2
True
"""
return json.dumps(data, cls=DataEncoder)


def json_load(fp):
"""Read COMPAS object data from a JSON file.
Parameters
----------
fp : file-like object or path
A writeable file-like object or the path to a file.
Returns
-------
data
The (COMPAS) data contained in the file.
Examples
--------
>>> import compas
>>> from compas.geometry import Point, Vector
>>> data1 = [Point(0, 0, 0), Vector(0, 0, 0)]
>>> compas.json_dump(data1, 'data.json')
>>> data2 = compas.json_load('data.json')
>>> data1 == data2
True
"""
if hasattr(fp, 'read'):
return json.load(fp, cls=DataDecoder)
with open(fp, 'r') as fp:
return json.load(fp, cls=DataDecoder)


def json_loads(s):
"""Read COMPAS object data from a JSON string.
Parameters
----------
s : str
A JSON data string.
Returns
-------
data
The (COMPAS) data contained in the string.
Examples
--------
>>> import compas
>>> from compas.geometry import Point, Vector
>>> data1 = [Point(0, 0, 0), Vector(0, 0, 0)]
>>> s = compas.json_dumps(data1)
>>> data2 = compas.json_loads()
>>> data1 == data2
True
"""
return json.loads(s, cls=DataDecoder)


# ==============================================================================
# Main
# ==============================================================================

if __name__ == '__main__':
import doctest

doctest.testmod(globs=globals())
6 changes: 5 additions & 1 deletion src/compas/geometry/transformations/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"""
import math

from compas.base import Base

from compas.geometry import multiply_matrices
from compas.geometry import transpose_matrix

Expand All @@ -29,7 +31,7 @@
__all__ = ['Transformation']


class Transformation(object):
class Transformation(Base):
"""The ``Transformation`` represents a 4x4 transformation matrix.

It is the base class for transformations like :class:`Rotation`,
Expand Down Expand Up @@ -60,6 +62,8 @@ class Transformation(object):
def __init__(self, matrix=None):
"""Construct a transformation from a 4x4 transformation matrix.
"""
super(Transformation, self).__init__()

if not matrix:
matrix = identity_matrix(4)
self.matrix = matrix
Expand Down
6 changes: 5 additions & 1 deletion src/compas/utilities/encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ class DataEncoder(json.JSONEncoder):
def default(self, o):
if hasattr(o, 'to_data'):
value = o.to_data()
if hasattr(o, 'dtype'):
dtype = o.dtype
else:
dtype = "{}/{}".format(".".join(o.__class__.__module__.split(".")[:-1]), o.__class__.__name__)
return {
'dtype': "{}/{}".format(".".join(o.__class__.__module__.split(".")[:-1]), o.__class__.__name__),
'dtype': dtype,
'value': value
}

Expand Down
56 changes: 56 additions & 0 deletions tests/compas/test_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import compas

from compas.geometry import Point, Vector, Frame
from compas.geometry import Box
from compas.geometry import Transformation

from compas.datastructures import Mesh


def test_json_native():
before = [[], (), {}, '', 1, 1.0, True, None]
after = compas.json_loads(compas.json_dumps(before))
assert after == [[], [], {}, '', 1, 1.0, True, None]


if not compas.IPY:
import numpy as np

def test_json_numpy():
before = [np.array([1, 2, 3]), np.array([1.0, 2.0, 3.0]), np.float64(1.0), np.int32(1)]
after = compas.json_loads(compas.json_dumps(before))
assert after == [[1, 2, 3], [1.0, 2.0, 3.0], 1.0, 1]


def test_json_primitive():
before = Point(0, 0, 0)
after = compas.json_loads(compas.json_dumps(before))
assert before.dtype == after.dtype
assert all(a == b for a, b in zip(before, after))


def test_json_shape():
before = Box(Frame(Point(0, 0, 0), Vector(1, 0, 0), Vector(0, 1, 0)), 1, 1, 1)
after = compas.json_loads(compas.json_dumps(before))
assert before.dtype == after.dtype
assert all(a == b for a, b in zip(before.vertices, after.vertices))


def test_json_xform():
before = Transformation.from_frame_to_frame(Frame.worldXY(), Frame.worldXY())
after = compas.json_loads(compas.json_dumps(before))
assert before.dtype == after.dtype
assert all(a == b for a, b in zip(before, after))


def test_json_mesh():
before = Mesh.from_vertices_and_faces([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], [[0, 1, 2, 3]])
after = compas.json_loads(compas.json_dumps(before))
assert before.dtype == after.dtype
assert all(before.has_vertex(vertex) for vertex in after.vertices())
assert all(after.has_vertex(vertex) for vertex in before.vertices())
assert all(before.has_face(face) for face in after.faces())
assert all(after.has_face(face) for face in before.faces())
assert all(before.has_edge(edge) for edge in after.edges())
assert all(after.has_edge(edge) for edge in before.edges())
assert all(before.face_vertices(a) == after.face_vertices(b) for a, b in zip(before.faces(), after.faces()))