Repository: jriddy/zerial

Issue:
Numpy arrays coming from serialization are not writable
Looks like `np.frombuffer` keeps the `owndata` flag as `False`, which prevents the array from becoming writable.

Discussion:

Patch:
diff --git a/src/zerial/numpy.py b/src/zerial/numpy.py
index e5f936e..5431318 100644
--- a/src/zerial/numpy.py
+++ b/src/zerial/numpy.py
@@ -33,8 +33,6 @@ if sys.version_info >= (3, 0):
             return x.decode()
         else:
             return str(x)
-
-    buffer_to_ndarray = np.frombuffer
 else:
     numpy_required_passthrus = DEFAULT_PASSTHRUS
 
@@ -47,14 +45,14 @@ else:
 
     num_to_bytes = lambda obj: memoryview(obj.data)
 
-    buffer_to_ndarray = lambda v, *xs, **kw: np.frombuffer(
-        v.tobytes() if isinstance(v, memoryview) else v,
-        *xs,
-        **kw
-    )
+    tostr = lambda x: x
 
-    def tostr(x):
-        return x
+
+def buffer_to_ndarray(v, *xs, **kw):
+    v = v.tobytes() if isinstance(v, memoryview) else v
+    # We have to copy to get an "owned", writable array
+    # TODO: find a better way around this that isn't as expensive
+    return np.frombuffer(v, *xs, **kw).copy()
 
 
 @attr.s


Test patch:
diff --git a/tests/test_numpy.py b/tests/test_numpy.py
new file mode 100644
index 0000000..9f46692
--- /dev/null
+++ b/tests/test_numpy.py
@@ -0,0 +1,82 @@
+import pytest
+
+import attr
+import numpy as np
+
+from zerial.numpy import NdarrayEncoder, the_numpy_structurer
+
+
+@pytest.fixture
+def encoder():
+    return NdarrayEncoder()
+
+
+@pytest.fixture
+def npztr():
+    return the_numpy_structurer
+
+
+def test_encoder_barfs_on_refcounted_python_objects_in_array(encoder, npztr):
+    arr = np.array([object()])
+    with pytest.raises(TypeError) as ce:
+        encoder.encode(arr, npztr)
+    assert str(ce.value).startswith("cowardly refusing")
+
+
+@pytest.mark.parametrize('arr', [
+    np.array([]),
+    np.empty((1024, 256, 16), dtype=np.float64),
+    np.random.random((10, 10, 10)),
+    np.random.random((5, 15, 2)).T,
+    # TODO: add more tests here
+])
+def test_encode_decode_stability(arr, encoder, npztr):
+    encoded = encoder.encode(arr, npztr)
+    if isinstance(encoded['%data'], memoryview):
+        # force memory read
+        encoded['%data'] = encoded['%data'].tobytes()
+    decoded = encoder.decode(encoded, npztr)
+    assert (arr == decoded).all()
+
+
+def test_encodes_toplevel_when_given_as_type(npztr, encoder):
+    arr = np.array(range(10), dtype=np.int_)
+    des = npztr.destructure(arr, np.ndarray)
+    enc = encoder.encode(arr, npztr)
+    assert des == enc
+
+
+def test_encodes_toplevel_fails_when_not_given_as_type(npztr, encoder):
+    arr = np.linspace(0, 1)
+    with pytest.raises(attr.exceptions.NotAnAttrsClassError):
+        npztr.destructure(arr)
+
+
+@attr.s(eq=False)
+class Example(object):
+    arr = attr.ib(type=np.ndarray)
+    other = attr.ib(type=str, default='')
+
+    def __eq__(self, other):
+        return (
+            self.__class__ == other.__class__ and
+            self.other == other.other and
+            (self.arr == other.arr).all()
+        )
+
+
+def test_can_encode_structured_ndarray(npztr):
+    example = Example(np.array([1, 19, 18]))
+    destructed = npztr.destructure(example)
+    restructed = npztr.restructure(Example, destructed)
+    assert example == restructed
+
+
+def test_restructured_array_is_writable(npztr):
+    arr = np.array([1, 2], dtype=np.int_)
+    example = Example(arr)
+    destructured = npztr.destructure(example)
+    restructured = npztr.restructure(Example, destructured)
+    new_arr = restructured.arr
+    new_arr += 2
+    assert ((arr + 2) == new_arr).all()