Repository: numpy/numpy

Issue:
Regression: array2string breaks for some subclasses
EDIT: the below may not be complete; there seems to be other breakage for structured dtypes.

The guard against infinite recursion in array2string done in #10544 causes problems for subclasses that count on user-defined formatters to have access to items of their own class; see https://github.com/astropy/astropy/issues/7214.  In particular, astropy's `Angle` class assumed that inside array2print, any item selection would be done using its `__getitem__`, so that even scalars would be returned as array scalars with all the appropriate methods. This assumption broke by the introduction of an `asarray` in #10544. As noted in https://github.com/numpy/numpy/pull/10544#discussion_r170435274, it may be possible to solve this by doing `asarray` inside the numpy formatters, rather than upon entry in `array2print`.

Here's a minimal testcase:
```
import numpy as np

class DuckCounter(np.ndarray):
    def __getitem__(self, item):
        result = super(DuckCounter, self).__getitem__(item)
        if not isinstance(result, DuckCounter):
            result = result[...].view(DuckCounter)
        return result
            
    def to_string(self):
        # only on scalers in this example...
        return {0: 'zero', 1: 'one', 2: 'two'}.get(self.item(), 'many')
    
    def __str__(self):
        if self.shape == ():
            return self.to_string()
        else:
            return np.array2string(self,
                                   formatter={'all': lambda x: x.to_string()})


dc = np.arange(5).view(DuckCounter)
print(dc)
# [zero one two many many]
```

On numpy 1.14.1, this gives
```
AttributeError: 'numpy.int64' object has no attribute 'to_string'
```


Discussion:

Resolving patch:
diff --git a/numpy/core/arrayprint.py b/numpy/core/arrayprint.py
--- a/numpy/core/arrayprint.py
+++ b/numpy/core/arrayprint.py
@@ -438,14 +438,15 @@ def _array2string(a, options, separator=' ', prefix=""):
     # The formatter __init__s in _get_format_function cannot deal with
     # subclasses yet, and we also need to avoid recursion issues in
     # _formatArray with subclasses which return 0d arrays in place of scalars
-    a = asarray(a)
+    data = asarray(a)
+    if a.shape == ():
+        a = data
 
     if a.size > options['threshold']:
         summary_insert = "..."
-        data = _leading_trailing(a, options['edgeitems'])
+        data = _leading_trailing(data, options['edgeitems'])
     else:
         summary_insert = ""
-        data = a
 
     # find the right formatting function for the array
     format_function = _get_format_function(data, **options)


Test patch:
diff --git a/numpy/core/tests/test_arrayprint.py b/numpy/core/tests/test_arrayprint.py
--- a/numpy/core/tests/test_arrayprint.py
+++ b/numpy/core/tests/test_arrayprint.py
@@ -5,7 +5,7 @@
 
 import numpy as np
 from numpy.testing import (
-     run_module_suite, assert_, assert_equal, assert_raises, assert_warns
+     run_module_suite, assert_, assert_equal, assert_raises, assert_warns, dec
 )
 import textwrap
 
@@ -34,6 +34,27 @@ class sub(np.ndarray): pass
             "     [(1,), (1,)]], dtype=[('a', '<i4')])"
         )
 
+    @dec.knownfailureif(True, "See gh-10544")
+    def test_object_subclass(self):
+        class sub(np.ndarray):
+            def __new__(cls, inp):
+                obj = np.asarray(inp).view(cls)
+                return obj
+
+            def __getitem__(self, ind):
+                ret = super(sub, self).__getitem__(ind)
+                return sub(ret)
+
+        # test that object + subclass is OK:
+        x = sub([None, None])
+        assert_equal(repr(x), 'sub([None, None], dtype=object)')
+        assert_equal(str(x), '[None None]')
+
+        x = sub([None, sub([None, None])])
+        assert_equal(repr(x),
+            'sub([None, sub([None, None], dtype=object)], dtype=object)')
+        assert_equal(str(x), '[None sub([None, None], dtype=object)]')
+
     def test_0d_object_subclass(self):
         # make sure that subclasses which return 0ds instead
         # of scalars don't cause infinite recursion in str
@@ -73,15 +94,27 @@ def __getitem__(self, ind):
         assert_equal(repr(x), 'sub(sub(None, dtype=object), dtype=object)')
         assert_equal(str(x), 'None')
 
-        # test that object + subclass is OK:
-        x = sub([None, None])
-        assert_equal(repr(x), 'sub([None, None], dtype=object)')
-        assert_equal(str(x), '[None None]')
-
-        x = sub([None, sub([None, None])])
-        assert_equal(repr(x),
-            'sub([None, sub([None, None], dtype=object)], dtype=object)')
-        assert_equal(str(x), '[None sub([None, None], dtype=object)]')
+        # gh-10663
+        class DuckCounter(np.ndarray):
+            def __getitem__(self, item):
+                result = super(DuckCounter, self).__getitem__(item)
+                if not isinstance(result, DuckCounter):
+                    result = result[...].view(DuckCounter)
+                return result
+
+            def to_string(self):
+                return {0: 'zero', 1: 'one', 2: 'two'}.get(self.item(), 'many')
+
+            def __str__(self):
+                if self.shape == ():
+                    return self.to_string()
+                else:
+                    fmt = {'all': lambda x: x.to_string()}
+                    return np.array2string(self, formatter=fmt)
+
+        dc = np.arange(5).view(DuckCounter)
+        assert_equal(str(dc), "[zero one two many many]")
+        assert_equal(str(dc[0]), "zero")
 
     def test_self_containing(self):
         arr0d = np.array(None)