Repository: ipython/ipython

Issue:
Autoreload fails because of ambiguity of truth values by comparing dictionaries containing Numpy arrays as attributes of a class.
In the case a class has a dictionary as attribute containing Numpy arrays, the comparison in line 293 fails as Numpy arrays generate an array if compared. 

Traceback:

```Python
[autoreload of FOOBAR failed: Traceback (most recent call last):
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 245, in check
    superreload(m, reload, self.old_objects)
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 413, in superreload
    update_generic(old_obj, new_obj)
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 350, in update_generic
    update(a, b)
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 304, in update_class
    raise e
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 293, in update_class
    if (old_obj == new_obj) is True:
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
]
```

example classes which generates this error are:

```Python
import numpy

class foo:
    array = {1:numpy.array([1,1]),2:numpy.array([2,2])}

class bar:
    def __init__(self):
        self.array = {1:numpy.array([1,1]),2:numpy.array([2,2])}
```

One can break down this to the comparison of the two dict which fails:

```Python
import numpy
{1: numpy.array([1, 1]), 2: numpy.array([1, 2])} == {1: numpy.array([1, 1]), 2: numpy.array([1, 2])}
```

It might be that defining Numpy arrays within a code is bad coding practise and should be avoided generally, but its not clear if this comparison could fail in other iterables and if one should paste a more general and robust comparison here. 


Discussion:
It's probably just me being thick, but I'm failing to reproduce this. Could you please show exactly what input needs to be given to IPython for this to trigger (let's say that the snippet with classes is in `test_reload.py`)? What version of IPython are you using?
You are right I haven't read the contributing guidelines carefully enough, sorry.  

My output from 'python -c "import IPython; print(IPython.sys_info())"'

```bash
{'commit_hash': '0dcb1debc',
 'commit_source': 'installation',
 'default_encoding': 'utf-8',
 'ipython_path': '/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython',
 'ipython_version': '7.21.0',
 'os_name': 'posix',
 'platform': 'macOS-11.2.3-x86_64-i386-64bit',
 'sys_executable': '/Users/USER/.pyenv/versions/3.9.2/bin/python',
 'sys_platform': 'darwin',
 'sys_version': '3.9.2 (default, Mar 24 2021, 13:17:25) \n'
                '[Clang 12.0.0 (clang-1200.0.32.29)]'}
```
I also encountering this from this platform:
```bash
{'commit_hash': '2486838d9',
 'commit_source': 'installation',
 'default_encoding': 'utf-8',
 'ipython_path': '/home/USER/anaconda3/envs/uc/lib/python3.7/site-packages/IPython',
 'ipython_version': '7.16.1',
 'os_name': 'posix',
 'platform': 'Linux-5.3.0-62-generic_______________________',
 'sys_executable': '/home/USER/anaconda3/envs/uc/bin/python',
 'sys_platform': 'linux',
 'sys_version': '3.7.3 | packaged by conda-forge | (default, Dec  6 2019, '
                '08:54:18) \n'
                '[GCC 7.3.0]'}
```

Here are a module and a snippet you ca run with 'ipython -i  -- test_reload.ipy'
[test.zip](https://github.com/ipython/ipython/files/6254822/test.zip)

```python
➜  ~ ipython -i  -- test_reload.ipy
Python 3.9.2 (default, Mar 24 2021, 13:17:25) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.21.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: bar.array
Out[1]: {'bar': array([1, 1]), 2: array([2, 2])}
```

Now you change something in bar-class in 'test_reload_lib.py'.

```python
In [2]: bar.array
[autoreload of test_reload_lib failed: Traceback (most recent call last):
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 245, in check
    superreload(m, reload, self.old_objects)
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 414, in superreload
    update_generic(old_obj, new_obj)
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 351, in update_generic
    update(a, b)
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 304, in update_class
    raise e
  File "/Users/USER/.pyenv/versions/3.9.2/lib/python3.9/site-packages/IPython/extensions/autoreload.py", line 293, in update_class
    if (old_obj == new_obj) is True:
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
]
Out[2]: {'bar': array([1, 1]), 2: array([2, 2])}
```
If you add another except-block over the lines 290 to 300 in 'python3.9/site-packages/IPython/extensions/autoreload.py'
and print the new and old obj:

```python
def update_class(old, new):
        ...
        try:
            try:
                new_obj = getattr(new, key)
                # explicitly checking that comparison returns True to handle
                # cases where `==` doesn't return a boolean.
                if (old_obj == new_obj) is True:
                    continue
            except AttributeError:
                # obsolete attribute: remove it
                try:
                    delattr(old, key)
                except (AttributeError, TypeError):
                    pass
                continue
        except ValueError as e:
            print("old_obj: ",old_obj, "new_obj: ", new_obj)
            raise e
        ...
```
you get:
```python
old_obj:  {'foo': array([1, 1]), 2: array([2, 2])} new_obj:  {'foo': array([1, 1]), 2: array([2, 2])}
```

As I interpret this, it seems to be the comparison between this dicts. If they are hardcoded like this. If you parse it by e.g. initialisation it works fine. 
Please do not misunderstand me, it is clear for me, that hardcoding like this is bad coding practise for a developer, but by doing analysis this might be practical from my point of view. I only would like to suggest that it might be cool if the user could be warned that s/he should not hardcode like this, what is causing this problem. Which could save time.

Resolving patch:
diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py
--- a/IPython/extensions/autoreload.py
+++ b/IPython/extensions/autoreload.py
@@ -321,6 +321,10 @@ def update_class(old, new):
             except (AttributeError, TypeError):
                 pass
             continue
+        except ValueError:
+            # can't compare nested structures containing
+            # numpy arrays using `==`
+            pass
 
         if update_generic(old_obj, new_obj):
             continue


Test patch:
diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py
--- a/IPython/extensions/tests/test_autoreload.py
+++ b/IPython/extensions/tests/test_autoreload.py
@@ -29,6 +29,7 @@
 
 from IPython.extensions.autoreload import AutoreloadMagics
 from IPython.core.events import EventManager, pre_run_cell
+from IPython.testing.decorators import skipif_not_numpy
 
 if platform.python_implementation() == "PyPy":
     pytest.skip(
@@ -274,6 +275,40 @@ def power(self, p):
             with self.assertRaises(AttributeError):
                 self.shell.run_code(f"{object_name}.toto")
 
+    @skipif_not_numpy
+    def test_comparing_numpy_structures(self):
+        self.shell.magic_autoreload("2")
+        mod_name, mod_fn = self.new_module(
+            textwrap.dedent(
+                """
+                                import numpy as np
+                                class MyClass:
+                                    a = (np.array((.1, .2)),
+                                         np.array((.2, .3)))
+                            """
+            )
+        )
+        self.shell.run_code("from %s import MyClass" % mod_name)
+        self.shell.run_code("first = MyClass()")
+
+        # change property `a`
+        self.write_file(
+            mod_fn,
+            textwrap.dedent(
+                """
+                                import numpy as np
+                                class MyClass:
+                                    a = (np.array((.3, .4)),
+                                         np.array((.5, .6)))
+                            """
+            ),
+        )
+
+        with tt.AssertNotPrints(
+            ("[autoreload of %s failed:" % mod_name), channel="stderr"
+        ):
+            self.shell.run_code("pass")  # trigger another reload
+
     def test_autoload_newly_added_objects(self):
         self.shell.magic_autoreload("3")
         mod_code = """