Repository: numpy/numpy

Issue:
TypeError: Cannot change data-type for object array
If I try and read in the array contained in this npy file:

https://gist.github.com/astrofrog/8c2d188005f31e0bba36/raw/3065c8fa220a6eaccbff20565d0d520c07e5e7e6/test.npy

then try and print out the array, so:

``` python
import numpy as np
array = np.load('test.npy')
print(array)
```

I get:

```
Traceback (most recent call last):
  File "test2.py", line 5, in <module>
    print(array)
  File "/Volumes/Raptor/miniconda3/envs/dev/lib/python3.4/site-packages/numpy/core/numeric.py", line 1767, in array_str
    return array2string(a, max_line_width, precision, suppress_small, ' ', "", str)
  File "/Volumes/Raptor/miniconda3/envs/dev/lib/python3.4/site-packages/numpy/core/arrayprint.py", line 459, in array2string
    separator, prefix, formatter=formatter)
  File "/Volumes/Raptor/miniconda3/envs/dev/lib/python3.4/site-packages/numpy/core/arrayprint.py", line 329, in _array2string
    _summaryEdgeItems, summary_insert)[:-1]
  File "/Volumes/Raptor/miniconda3/envs/dev/lib/python3.4/site-packages/numpy/core/arrayprint.py", line 526, in _formatArray
    s += _formatArray(a[-i], format_function, rank-1, max_line_len,
  File "/Volumes/Raptor/miniconda3/envs/dev/lib/python3.4/site-packages/numpy/core/records.py", line 481, in __getitem__
    return obj.view(dtype=(self.dtype.type, obj.dtype.descr))
  File "/Volumes/Raptor/miniconda3/envs/dev/lib/python3.4/site-packages/numpy/core/records.py", line 540, in view
    return ndarray.view(self, dtype)
  File "/Volumes/Raptor/miniconda3/envs/dev/lib/python3.4/site-packages/numpy/core/records.py", line 457, in __setattr__
    raise exctype(value)
TypeError: Cannot change data-type for object array.
```

This is with the latest developer version of Numpy (3c5409e4e38e6034d69d0042bf2a3bc854ef2e53) and Python 3.4 on MacOS X.

The dtype can be printed, as can individual columns, but the array as a whole can't. This doesn't occur in the latest stable release so may be a regression?



Discussion:

Resolving patch:
diff --git a/numpy/core/_internal.py b/numpy/core/_internal.py
--- a/numpy/core/_internal.py
+++ b/numpy/core/_internal.py
@@ -305,6 +305,174 @@ def _index_fields(ary, fields):
     copy_dtype = {'names':view_dtype['names'], 'formats':view_dtype['formats']}
     return array(view, dtype=copy_dtype, copy=True)
 
+def _get_all_field_offsets(dtype, base_offset=0):
+    """ Returns the types and offsets of all fields in a (possibly structured)
+    data type, including nested fields and subarrays.
+
+    Parameters
+    ----------
+    dtype : data-type
+        Data type to extract fields from.
+    base_offset : int, optional
+        Additional offset to add to all field offsets.
+
+    Returns
+    -------
+    fields : list of (data-type, int) pairs
+        A flat list of (dtype, byte offset) pairs.
+
+    """
+    fields = []
+    if dtype.fields is not None:
+        for name in dtype.names:
+           sub_dtype = dtype.fields[name][0]
+           sub_offset = dtype.fields[name][1] + base_offset
+           fields.extend(_get_all_field_offsets(sub_dtype, sub_offset))
+    else:
+        if dtype.shape:
+            sub_offsets = _get_all_field_offsets(dtype.base, base_offset)
+            count = 1
+            for dim in dtype.shape:
+                count *= dim
+            fields.extend((typ, off + dtype.base.itemsize*j)
+                           for j in range(count) for (typ, off) in sub_offsets)
+        else:
+            fields.append((dtype, base_offset))
+    return fields
+
+def _check_field_overlap(new_fields, old_fields):
+    """ Perform object memory overlap tests for two data-types (see
+    _view_is_safe).
+
+    This function checks that new fields only access memory contained in old
+    fields, and that non-object fields are not interpreted as objects and vice
+    versa.
+
+    Parameters
+    ----------
+    new_fields : list of (data-type, int) pairs
+        Flat list of (dtype, byte offset) pairs for the new data type, as
+        returned by _get_all_field_offsets.
+    old_fields: list of (data-type, int) pairs
+        Flat list of (dtype, byte offset) pairs for the old data type, as
+        returned by _get_all_field_offsets.
+
+    Raises
+    ------
+    TypeError
+        If the new fields are incompatible with the old fields
+
+    """
+    from .numerictypes import object_
+    from .multiarray import dtype
+
+    #first go byte by byte and check we do not access bytes not in old_fields
+    new_bytes = set()
+    for tp, off in new_fields:
+        new_bytes.update(set(range(off, off+tp.itemsize)))
+    old_bytes = set()
+    for tp, off in old_fields:
+        old_bytes.update(set(range(off, off+tp.itemsize)))
+    if new_bytes.difference(old_bytes):
+        raise TypeError("view would access data parent array doesn't own")
+
+    #next check that we do not interpret non-Objects as Objects, and vv
+    obj_offsets = [off for (tp, off) in old_fields if tp.type is object_]
+    obj_size = dtype(object_).itemsize
+
+    for fld_dtype, fld_offset in new_fields:
+        if fld_dtype.type is object_:
+            # check we do not create object views where
+            # there are no objects.
+            if fld_offset not in obj_offsets:
+                raise TypeError("cannot view non-Object data as Object type")
+        else:
+            # next check we do not create non-object views
+            # where there are already objects.
+            # see validate_object_field_overlap for a similar computation.
+            for obj_offset in obj_offsets:
+                if (fld_offset < obj_offset + obj_size and
+                        obj_offset < fld_offset + fld_dtype.itemsize):
+                    raise TypeError("cannot view Object as non-Object type")
+
+def _getfield_is_safe(oldtype, newtype, offset):
+    """ Checks safety of getfield for object arrays.
+
+    As in _view_is_safe, we need to check that memory containing objects is not
+    reinterpreted as a non-object datatype and vice versa.
+
+    Parameters
+    ----------
+    oldtype : data-type
+        Data type of the original ndarray.
+    newtype : data-type
+        Data type of the field being accessed by ndarray.getfield
+    offset : int
+        Offset of the field being accessed by ndarray.getfield
+
+    Raises
+    ------
+    TypeError
+        If the field access is invalid
+
+    """
+    new_fields = _get_all_field_offsets(newtype, offset)
+    old_fields = _get_all_field_offsets(oldtype)
+    # raises if there is a problem
+    _check_field_overlap(new_fields, old_fields)
+
+def _view_is_safe(oldtype, newtype):
+    """ Checks safety of a view involving object arrays, for example when
+    doing::
+
+        np.zeros(10, dtype=oldtype).view(newtype)
+
+    We need to check that
+    1) No memory that is not an object will be interpreted as a object,
+    2) No memory containing an object will be interpreted as an arbitrary type.
+    Both cases can cause segfaults, eg in the case the view is written to.
+    Strategy here is to also disallow views where newtype has any field in a
+    place oldtype doesn't.
+
+    Parameters
+    ----------
+    oldtype : data-type
+        Data type of original ndarray
+    newtype : data-type
+        Data type of the view
+
+    Raises
+    ------
+    TypeError
+        If the new type is incompatible with the old type.
+
+    """
+    new_fields = _get_all_field_offsets(newtype)
+    new_size = newtype.itemsize
+
+    old_fields = _get_all_field_offsets(oldtype)
+    old_size = oldtype.itemsize
+
+    # if the itemsizes are not equal, we need to check that all the
+    # 'tiled positions' of the object match up. Here, we allow
+    # for arbirary itemsizes (even those possibly disallowed
+    # due to stride/data length issues).
+    if old_size ==  new_size:
+        new_num = old_num = 1
+    else:
+        gcd_new_old = _gcd(new_size, old_size)
+        new_num = old_size // gcd_new_old
+        old_num = new_size // gcd_new_old
+
+    # get position of fields within the tiling
+    new_fieldtile = [(tp, off + new_size*j)
+                     for j in range(new_num) for (tp, off) in new_fields]
+    old_fieldtile = [(tp, off + old_size*j)
+                     for j in range(old_num) for (tp, off) in old_fields]
+
+    # raises if there is a problem
+    _check_field_overlap(new_fieldtile, old_fieldtile)
+
 # Given a string containing a PEP 3118 format specifier,
 # construct a Numpy dtype
 


Test patch:
diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py
--- a/numpy/core/tests/test_multiarray.py
+++ b/numpy/core/tests/test_multiarray.py
@@ -809,6 +809,14 @@ def test_casting(self):
             t = [('a', '>i4'), ('b', '<f8'), ('c', 'i4')]
             assert_(not np.can_cast(a.dtype, t, casting=casting))
 
+    def test_objview(self):
+        # https://github.com/numpy/numpy/issues/3286
+        a = np.array([], dtype=[('a', 'f'), ('b', 'f'), ('c', 'O')])
+        a[['a', 'b']] # TypeError?
+
+        # https://github.com/numpy/numpy/issues/3253
+        dat2 = np.zeros(3, [('A', 'i'), ('B', '|O')])
+        new2 = dat2[['B', 'A']] # TypeError?
 
 class TestBool(TestCase):
     def test_test_interning(self):
@@ -3577,8 +3585,9 @@ def test_record_no_hash(self):
 
 class TestView(TestCase):
     def test_basic(self):
-        x = np.array([(1, 2, 3, 4), (5, 6, 7, 8)], dtype=[('r', np.int8), ('g', np.int8),
-                                                  ('b', np.int8), ('a', np.int8)])
+        x = np.array([(1, 2, 3, 4), (5, 6, 7, 8)],
+                      dtype=[('r', np.int8), ('g', np.int8),
+                             ('b', np.int8), ('a', np.int8)])
         # We must be specific about the endianness here:
         y = x.view(dtype='<i4')
         # ... and again without the keyword.
@@ -5523,6 +5532,89 @@ def test_collections_hashable(self):
         x = np.array([])
         self.assertFalse(isinstance(x, collections.Hashable))
 
+from numpy.core._internal import _view_is_safe
+
+class TestObjViewSafetyFuncs:
+    def test_view_safety(self):
+        psize = dtype('p').itemsize
+
+        # creates dtype but with extra character code - for missing 'p' fields
+        def mtype(s):
+            n, offset, fields = 0, 0, []
+            for c in s.split(','): #subarrays won't work
+                if c != '-':
+                    fields.append(('f{0}'.format(n), c, offset))
+                    n += 1
+                offset += dtype(c).itemsize if c != '-' else psize
+
+            names, formats, offsets = zip(*fields)
+            return dtype({'names': names, 'formats': formats,
+                          'offsets': offsets, 'itemsize': offset})
+
+        # test nonequal itemsizes with objects:
+        # these should succeed:
+        _view_is_safe(dtype('O,p,O,p'), dtype('O,p,O,p,O,p'))
+        _view_is_safe(dtype('O,O'), dtype('O,O,O'))
+        # these should fail:
+        assert_raises(TypeError, _view_is_safe, dtype('O,O,p'), dtype('O,O'))
+        assert_raises(TypeError, _view_is_safe, dtype('O,O,p'), dtype('O,p'))
+        assert_raises(TypeError, _view_is_safe, dtype('O,O,p'), dtype('p,O'))
+
+        # test nonequal itemsizes with missing fields:
+        # these should succeed:
+        _view_is_safe(mtype('-,p,-,p'), mtype('-,p,-,p,-,p'))
+        _view_is_safe(dtype('p,p'), dtype('p,p,p'))
+        # these should fail:
+        assert_raises(TypeError, _view_is_safe, mtype('p,p,-'), mtype('p,p'))
+        assert_raises(TypeError, _view_is_safe, mtype('p,p,-'), mtype('p,-'))
+        assert_raises(TypeError, _view_is_safe, mtype('p,p,-'), mtype('-,p'))
+
+        # scans through positions at which we can view a type
+        def scanView(d1, otype):
+            goodpos = []
+            for shift in range(d1.itemsize - dtype(otype).itemsize+1):
+                d2 = dtype({'names': ['f0'], 'formats': [otype],
+                            'offsets': [shift], 'itemsize': d1.itemsize})
+                try:
+                    _view_is_safe(d1, d2)
+                except TypeError:
+                    pass
+                else:
+                    goodpos.append(shift)
+            return goodpos
+
+        # test partial overlap with object field
+        assert_equal(scanView(dtype('p,O,p,p,O,O'), 'p'),
+                     [0] + list(range(2*psize, 3*psize+1)))
+        assert_equal(scanView(dtype('p,O,p,p,O,O'), 'O'),
+                     [psize, 4*psize, 5*psize])
+
+        # test partial overlap with missing field
+        assert_equal(scanView(mtype('p,-,p,p,-,-'), 'p'),
+                     [0] + list(range(2*psize, 3*psize+1)))
+
+        # test nested structures with objects:
+        nestedO = dtype([('f0', 'p'), ('f1', 'p,O,p')])
+        assert_equal(scanView(nestedO, 'p'), list(range(psize+1)) + [3*psize])
+        assert_equal(scanView(nestedO, 'O'), [2*psize])
+
+        # test nested structures with missing fields:
+        nestedM = dtype([('f0', 'p'), ('f1', mtype('p,-,p'))])
+        assert_equal(scanView(nestedM, 'p'), list(range(psize+1)) + [3*psize])
+
+        # test subarrays with objects
+        subarrayO = dtype('p,(2,3)O,p')
+        assert_equal(scanView(subarrayO, 'p'), [0, 7*psize])
+        assert_equal(scanView(subarrayO, 'O'), 
+                     list(range(psize, 6*psize+1, psize)))
+
+        #test dtype with overlapping fields
+        overlapped = dtype({'names': ['f0', 'f1', 'f2', 'f3'],
+                            'formats': ['p', 'p', 'p', 'p'],
+                            'offsets': [0, 1, 3*psize-1, 3*psize],
+                            'itemsize': 4*psize})
+        assert_equal(scanView(overlapped, 'p'), [0, 1, 3*psize-1, 3*psize])
+
 
 if __name__ == "__main__":
     run_module_suite()
diff --git a/numpy/core/tests/test_records.py b/numpy/core/tests/test_records.py
--- a/numpy/core/tests/test_records.py
+++ b/numpy/core/tests/test_records.py
@@ -219,6 +219,15 @@ def test_pickle_2(self):
         assert_equal(a, pickle.loads(pickle.dumps(a)))
         assert_equal(a[0], pickle.loads(pickle.dumps(a[0])))
 
+    def test_objview_record(self):
+        # https://github.com/numpy/numpy/issues/2599
+        dt = np.dtype([('foo', 'i8'), ('bar', 'O')])
+        r = np.zeros((1,3), dtype=dt).view(np.recarray)
+        r.foo = np.array([1, 2, 3]) # TypeError?
+
+        # https://github.com/numpy/numpy/issues/3256
+        ra = np.recarray((2,), dtype=[('x', object), ('y', float), ('z', int)])
+        ra[['x','y']] #TypeError?
 
 def test_find_duplicate():
     l1 = [1, 2, 3, 4, 5, 6]
diff --git a/numpy/core/tests/test_regression.py b/numpy/core/tests/test_regression.py
--- a/numpy/core/tests/test_regression.py
+++ b/numpy/core/tests/test_regression.py
@@ -1739,10 +1739,9 @@ def test_zerosize_accumulate(self):
         assert_equal(np.add.accumulate(x[:-1, 0]), [])
 
     def test_objectarray_setfield(self):
-        # Setfield directly manipulates the raw array data,
-        # so is invalid for object arrays.
+        # Setfield should not overwrite Object fields with non-Object data
         x = np.array([1, 2, 3], dtype=object)
-        assert_raises(RuntimeError, x.setfield, 4, np.int32, 0)
+        assert_raises(TypeError, x.setfield, 4, np.int32, 0)
 
     def test_setting_rank0_string(self):
         "Ticket #1736"
diff --git a/numpy/lib/tests/test_recfunctions.py b/numpy/lib/tests/test_recfunctions.py
--- a/numpy/lib/tests/test_recfunctions.py
+++ b/numpy/lib/tests/test_recfunctions.py
@@ -700,6 +700,36 @@ def test_two_keys_two_vars(self):
         assert_equal(test.dtype, control.dtype)
         assert_equal(test, control)
 
+class TestAppendFieldsObj(TestCase):
+    """
+    Test append_fields with arrays containing objects
+    """
+    # https://github.com/numpy/numpy/issues/2346
+
+    def setUp(self):
+        from datetime import date
+        self.data = dict(obj=date(2000, 1, 1))
+
+    def test_append_to_objects(self):
+        "Test append_fields when the base array contains objects"
+        obj = self.data['obj']
+        x = np.array([(obj, 1.), (obj, 2.)],
+                      dtype=[('A', object), ('B', float)])
+        y = np.array([10, 20], dtype=int)
+        test = append_fields(x, 'C', data=y, usemask=False)
+        control = np.array([(obj, 1.0, 10), (obj, 2.0, 20)],
+                           dtype=[('A', object), ('B', float), ('C', int)])
+        assert_equal(test, control)
+
+    def test_append_with_objects(self):
+        "Test append_fields when the appended data contains objects"
+        obj = self.data['obj']
+        x = np.array([(10, 1.), (20, 2.)], dtype=[('A', int), ('B', float)])
+        y = np.array([obj, obj], dtype=object)
+        test = append_fields(x, 'C', data=y, dtypes=object, usemask=False)
+        control = np.array([(10, 1.0, obj), (20, 2.0, obj)],
+                           dtype=[('A', int), ('B', float), ('C', object)])
+        assert_equal(test, control)
 
 if __name__ == '__main__':
     run_module_suite()