diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index dafae4c..306bb8b 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -269,7 +269,7 @@ def update_function(old, new): def update_class(old, new): """Replace stuff in the __dict__ of a class, and upgrade - method code objects""" + method code objects, and add new methods, if any""" for key in list(old.__dict__.keys()): old_obj = getattr(old, key) try: @@ -291,6 +291,13 @@ def update_class(old, new): except (AttributeError, TypeError): pass # skip non-writable attributes + for key in list(new.__dict__.keys()): + if key not in list(old.__dict__.keys()): + try: + setattr(old, key, getattr(new, key)) + except (AttributeError, TypeError): + pass # skip non-writable attributes + def update_property(old, new): """Replace get/set/del functions of a property""" diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 89c288b..a942c5e 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -151,6 +151,64 @@ class TestAutoreload(Fixture): with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): self.shell.run_code("pass") # trigger another reload + def test_reload_class_attributes(self): + self.shell.magic_autoreload("2") + mod_name, mod_fn = self.new_module(textwrap.dedent(""" + class MyClass: + + def __init__(self, a=10): + self.a = a + self.b = 22 + # self.toto = 33 + + def square(self): + print('compute square') + return self.a*self.a + """ + ) + ) + self.shell.run_code("from %s import MyClass" % mod_name) + self.shell.run_code("first = MyClass(5)") + self.shell.run_code("first.square()") + with nt.assert_raises(AttributeError): + self.shell.run_code("first.cube()") + with nt.assert_raises(AttributeError): + self.shell.run_code("first.power(5)") + self.shell.run_code("first.b") + with nt.assert_raises(AttributeError): + self.shell.run_code("first.toto") + + # remove square, add power + + self.write_file( + mod_fn, + textwrap.dedent( + """ + class MyClass: + + def __init__(self, a=10): + self.a = a + self.b = 11 + + def power(self, p): + print('compute power '+str(p)) + return self.a**p + """ + ), + ) + + self.shell.run_code("second = MyClass(5)") + + for object_name in {'first', 'second'}: + self.shell.run_code("{object_name}.power(5)".format(object_name=object_name)) + with nt.assert_raises(AttributeError): + self.shell.run_code("{object_name}.cube()".format(object_name=object_name)) + with nt.assert_raises(AttributeError): + self.shell.run_code("{object_name}.square()".format(object_name=object_name)) + self.shell.run_code("{object_name}.b".format(object_name=object_name)) + self.shell.run_code("{object_name}.a".format(object_name=object_name)) + with nt.assert_raises(AttributeError): + self.shell.run_code("{object_name}.toto".format(object_name=object_name)) def _check_smoketest(self, use_aimport=True): """ @@ -340,3 +398,7 @@ x = -99 def test_smoketest_autoreload(self): self._check_smoketest(use_aimport=False) + + + + diff --git a/IPython/testing/iptest.py b/IPython/testing/iptest.py index 633cb72..445e826 100644 --- a/IPython/testing/iptest.py +++ b/IPython/testing/iptest.py @@ -386,6 +386,12 @@ def run_iptest(): monkeypatch_xunit() arg1 = sys.argv[1] + if arg1.startswith('IPython/'): + if arg1.endswith('.py'): + arg1 = arg1[:-3] + sys.argv[1] = arg1.replace('/', '.') + + arg1 = sys.argv[1] if arg1 in test_sections: section = test_sections[arg1] sys.argv[1:2] = section.includes diff --git a/docs/source/whatsnew/pr/improve-autoreload.md b/docs/source/whatsnew/pr/improve-autoreload.md new file mode 100644 index 0000000..7062299 --- /dev/null +++ b/docs/source/whatsnew/pr/improve-autoreload.md @@ -0,0 +1,30 @@ +magic `%autoreload 2` now captures new methods added to classes. Earlier, only methods existing as of the initial import were being tracked and updated. + +This new feature helps dual environement development - Jupyter+IDE - where the code gradually moves from notebook cells to package files, as it gets structured. + +**Example**: An instance of the class `MyClass` will be able to access the method `cube()` after it is uncommented and the file `file1.py` saved on disk. + +````python +# notebook + +from mymodule import MyClass +first = MyClass(5) +```` + +````python +# mymodule/file1.py + +class MyClass: + + def __init__(self, a=10): + self.a = a + + def square(self): + print('compute square') + return self.a*self.a + + # def cube(self): + # print('compute cube') + # return self.a*self.a*self.a +```` +