Repository: numpy/numpy

Issue:
BUG: np.void(b'test') enters recursion loop in repr
Regression in #8983, perhaps. @ahaldane?


Discussion:
Yes, I have come across some infinite recurions as a result of #8983 too. But I didn't want to work on a fix yet since I think #8981 (perhaps also #9332) will fix it, hopefully soon.
~~LGTM. Either update or let me know you don't want to change the test, and I think we can merge.~~

wrong issue, sorry
Any progress on this? It seems like a serious regression
This issue is causing a ``RuntimeError('maximum recursion depth exceeded',)`` when calling ``testing.assert_equal`` on a ``np.void`` in the ``build_err_msg`` function, i.e. in ``test_scalar_copy`` from ``test_regression.py`` which was added lately. Lucky for CPython that can be caught, unfortunately it crashes the PyPy interpreter.
I think some serious thought needs to go into which code has the job of printing arrays and scalars. We're in this mess because:

* Some scalars do `repr(np.array(self))
* Some arrays do `repr(arr[idx])` on each item

Of course, this gives a potential for co-recursion.
Unfortunately, in 1.13, the break in this co-recursion depended on the data type, and the regression accidently assumed it was cut in the same place each time
It'd be great if we could make this link unconditionally cut.

Resolving patch:
diff --git a/numpy/core/arrayprint.py b/numpy/core/arrayprint.py
--- a/numpy/core/arrayprint.py
+++ b/numpy/core/arrayprint.py
@@ -309,13 +309,7 @@ def _get_format_function(data, **options):
     """
     dtype_ = data.dtype
     if dtype_.fields is not None:
-        format_functions = []
-        for field_name in dtype_.names:
-            format_function = _get_format_function(data[field_name], **options)
-            if dtype_[field_name].shape != ():
-                format_function = SubArrayFormat(format_function)
-            format_functions.append(format_function)
-        return StructureFormat(format_functions)
+        return StructureFormat.from_data(data, **options)
 
     dtypeobj = dtype_.type
     formatdict = _get_formatdict(data, **options)
@@ -873,6 +867,20 @@ def __init__(self, format_functions):
         self.format_functions = format_functions
         self.num_fields = len(format_functions)
 
+    @classmethod
+    def from_data(cls, data, **options):
+        """
+        This is a second way to initialize StructureFormat, using the raw data
+        as input. Added to avoid changing the signature of __init__.
+        """
+        format_functions = []
+        for field_name in data.dtype.names:
+            format_function = _get_format_function(data[field_name], **options)
+            if data.dtype[field_name].shape != ():
+                format_function = SubArrayFormat(format_function)
+            format_functions.append(format_function)
+        return cls(format_functions)
+
     def __call__(self, x):
         s = "("
         for field, format_function in zip(x, self.format_functions):
@@ -880,6 +888,15 @@ def __call__(self, x):
         return (s[:-2] if 1 < self.num_fields else s[:-1]) + ")"
 
 
+def _void_scalar_repr(x):
+    """
+    Implements the repr for structured-void scalars. It is called from the
+    scalartypes.c.src code, and is placed here because it uses the elementwise
+    formatters defined above.
+    """
+    return StructureFormat.from_data(array(x), **_format_options)(x)
+
+
 _typelessdata = [int_, float_, complex_]
 if issubclass(intc, int):
     _typelessdata.append(intc)


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
@@ -60,6 +60,10 @@ def test_containing_list(self):
         assert_equal(repr(arr1d),
             'array([list([1, 2]), list([3])], dtype=object)')
 
+    def test_void_scalar_recursion(self):
+        # gh-9345
+        repr(np.void(b'test'))  # RecursionError ?
+
 
 class TestComplexArray(object):
     def test_str(self):