Repository: cython/cython

Issue:
[BUG] `CythonCImports` shadowing functionality can confuse pickle
### Describe the bug

Moving over from NumPy https://github.com/numpy/numpy/issues/20167.  On Cython, the `CythonCImports` shadowing module tries to import things that are below it using `__import__` in its `__getattr__`:
```python
    def __getattr__(self, item):
        if item.startswith('__') and item.endswith('__'):
            raise AttributeError(item)
        return __import__(item)
```

### Code to reproduce the behaviour:

```cython
import pickle
import numpy as np

# This adds e.g
# sys.modules['cython.cimports'] = CythonCImports('cython.cimports')
from Cython import Shadow

# This will cause the following to fail:
pickle.dumps(np.add)

# The reason is convoluted, but basically, sometimes pickle will search
# all modules for the attribute.  But this raises a ModuleNotFoundError
getattr(sys.modules["cython.cimports"], "add")
```

### Expected behaviour

Cython should ensure an AttributeError is given.  I suspect the simple fix is to do:
```
    try:
        __import__(item)
    except ModuleNotFoundError:
        raise AttributeError(item) from None
```
or something similar.  I am not quite sure what this code does, so there is probably a much better solution.  But since I suspect this usage is somewhat niche, the above would probably do.

### OS

_No response_

### Python version

_No response_

### Cython version

3

### Additional context

I will admit that NumPy is not being nice about not providing `__module__` on the ufunc object and that is an issue, but in the grand scheme of things, I also suspect that others will run into similar issues.


Discussion:
I hit this again when trying to use 3.0.0b3 with NumPy.
If I put a `pdb.set_trace()` just before the `__import__(name)`, I see it is tripped for the `run/pure_py3` test. It is called as `hasattr(module, x)` (where `x == 'math'`) in  [`importlib/_bootstrap.py::_handle_from_list`](https://github.com/python/cpython/blob/f3909b8bc83675b7ab093dbc558e677558d8e718/Lib/importlib/_bootstrap.py#L1231), which would strengthen the argument that a failure to import should raise an `AttributeError`, not a `ModuleNotFoundError`.

Resolving patch:
diff --git a/Cython/Shadow.py b/Cython/Shadow.py
--- a/Cython/Shadow.py
+++ b/Cython/Shadow.py
@@ -551,7 +551,6 @@ def __getattr__(self, attr):
         sys.modules['cython.%s' % self.__name__] = mod
         return getattr(mod, attr)
 
-
 class CythonCImports(object):
     """
     Simplistic module mock to make cimports sort-of work in Python code.
@@ -565,7 +564,14 @@ def __init__(self, module):
     def __getattr__(self, item):
         if item.startswith('__') and item.endswith('__'):
             raise AttributeError(item)
-        return __import__(item)
+        try:
+            return __import__(item)
+        except ImportError:
+            import sys
+            ex = AttributeError(item)
+            if sys.version_info >= (3, 0):
+                ex.__cause__ = None
+            raise ex
 
 
 import math, sys


Test patch:
diff --git a/tests/run/test_shadow_error.py b/tests/run/test_shadow_error.py
new file mode 100644
--- /dev/null
+++ b/tests/run/test_shadow_error.py
@@ -0,0 +1,14 @@
+# mode: run
+# tag: numpy, gh5411
+
+import pickle
+import numpy as np
+
+# This adds e.g
+# sys.modules['cython.cimports'] = CythonCImports('cython.cimports')
+from Cython import Shadow
+
+# This calls hasattr(CythonCImports, "add")
+add = pickle.loads(pickle.dumps(np.add))
+
+assert add is np.add