Repository: numpy/numpy

Issue:
Can't pickle UFuncTypeError
numpy 1.18.4-py38h8854b6b_0 (conda-forge)
Linux x64

```python
import pickle
import numpy


try:
    numpy.ones(1) * "foo"
except Exception as e:
    pickle.dumps(e)
```
Output:
```
Traceback (most recent call last):
  File "ufunctypeerror.py", line 6, in <module>
    numpy.ones(1) * "foo"
numpy.core._exceptions.UFuncTypeError: ufunc 'multiply' did not contain a loop with signature matching types (dtype('<U32'), dtype('<U32')) -> dtype('<U32')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "ufunctypeerror.py", line 8, in <module>
    pickle.dumps(e)
_pickle.PicklingError: Can't pickle <class 'numpy.core._exceptions.UFuncTypeError'>: it's not the same object as numpy.core._exceptions.UFuncTypeError
```




Discussion:
The error seems to come from:

https://github.com/python/cpython/blob/df773f8c5454acebe08c31e7308597fa5a8bf5df/Modules/_pickle.c#L3581-L3650

I can't help feeling there's a bug in the error message formatting here by python itself.
Ah, I see the problem now. It's `@_display_as_base` causing the issue, and the simplest repro is:
```python
import pickle
from numpy.core._exceptions import _UFuncNoLoopError
pickle.dumps(_UFuncNoLoopError)
```
Hmmm, do we have to set the qualname as well to make printing nice?
If we want to keep digging, we could attach a metaclass to override pickling...

I would prefer if we can avoid it if it has no big issues (not sure if we can set a pickler externally, but I guess that is also type based). Printing seems fine without qualname, since it seems to rely on `__name__`. I suppose there may be subtleties where the qualname shines through?
It sounds like we want to drop both of these lines then:

https://github.com/numpy/numpy/blob/fb314a390851d4c21f3f6a2a87cffd329219c524/numpy/core/_exceptions.py#L29-L30
I tried, and it pickles if I just drop the qualname, so maybe its fine?
The module setting is currently a no-op anyway, but in all the cases when it isn't it would also break pickling.
This also happens with `MemoryError` arising from allocating an array.
Hmmm, is this urgent enough to be fixed for 1.19.3? It seems like this may be problematic in some schemes of multiprocessing!

Resolving patch:
diff --git a/numpy/core/_exceptions.py b/numpy/core/_exceptions.py
--- a/numpy/core/_exceptions.py
+++ b/numpy/core/_exceptions.py
@@ -26,8 +26,6 @@ def _display_as_base(cls):
     """
     assert issubclass(cls, Exception)
     cls.__name__ = cls.__base__.__name__
-    cls.__qualname__ = cls.__base__.__qualname__
-    set_module(cls.__base__.__module__)(cls)
     return cls
 
 


Test patch:
diff --git a/numpy/core/tests/test__exceptions.py b/numpy/core/tests/test__exceptions.py
--- a/numpy/core/tests/test__exceptions.py
+++ b/numpy/core/tests/test__exceptions.py
@@ -1,11 +1,21 @@
 """
 Tests of the ._exceptions module. Primarily for exercising the __str__ methods.
 """
+
+import pickle
+
 import numpy as np
 
 _ArrayMemoryError = np.core._exceptions._ArrayMemoryError
+_UFuncNoLoopError = np.core._exceptions._UFuncNoLoopError
 
 class TestArrayMemoryError:
+    def test_pickling(self):
+        """ Test that _ArrayMemoryError can be pickled """
+        error = _ArrayMemoryError((1023,), np.dtype(np.uint8))
+        res = pickle.loads(pickle.dumps(error))
+        assert res._total_size == error._total_size
+
     def test_str(self):
         e = _ArrayMemoryError((1023,), np.dtype(np.uint8))
         str(e)  # not crashing is enough
@@ -40,3 +50,9 @@ def test__total_size(self):
 
         e = _ArrayMemoryError((2, 4), np.dtype((np.uint64, 16)))
         assert e._total_size == 1024
+
+
+class TestUFuncNoLoopError:
+    def test_pickling(self):
+        """ Test that _UFuncNoLoopError can be pickled """
+        assert isinstance(pickle.dumps(_UFuncNoLoopError), bytes)