From 1d3018a93e98ad55f41d4419f835b738de80e1b7 2020-12-17 13:23:15 From: Spas Kalaydzhisyki Date: 2020-12-17 13:23:15 Subject: [PATCH] Add new '%autoreload 3' option Example: When an IPython session is ran with the 'autoreload' extension, you will now have the option '3' to select which means the following: 1. replicate all functionality from option 2 2. autoload all new funcs/classes/enums/globals from the module when they're added 3. autoload all newly imported funcs/classes/enums/globals from external modules Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload`` For more information please see unit test - extensions/tests/test_autoreload.py : 'test_autoload_newly_added_objects' --- diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index ada680f..1b86e0c 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -48,6 +48,12 @@ The following magic commands are provided: Reload all modules (except those excluded by ``%aimport``) every time before executing the Python code typed. +``%autoreload 3`` + + Reload all modules AND autoload newly added objects + (except those excluded by ``%aimport``) + every time before executing the Python code typed. + ``%aimport`` List modules which are to be automatically imported or not to be imported. @@ -131,7 +137,10 @@ class ModuleReloader(object): check_all = True """Autoreload all modules, not just those listed in 'modules'""" - def __init__(self): + autoload_obj = False + """Autoreload all modules AND autoload all new objects""" + + def __init__(self, shell=None): # Modules that failed to reload: {module: mtime-on-failed-reload, ...} self.failed = {} # Modules specially marked as autoreloadable. @@ -142,6 +151,7 @@ class ModuleReloader(object): self.old_objects = {} # Module modification timestamps self.modules_mtimes = {} + self.shell = shell # Cache module modification times self.check(check_all=True, do_reload=False) @@ -242,7 +252,10 @@ class ModuleReloader(object): # If we've reached this point, we should try to reload the module if do_reload: try: - superreload(m, reload, self.old_objects) + if self.autoload_obj: + superreload(m, reload, self.old_objects, self.shell) + else: + superreload(m, reload, self.old_objects) if py_filename in self.failed: del self.failed[py_filename] except: @@ -356,7 +369,25 @@ class StrongRef(object): return self.obj -def superreload(module, reload=reload, old_objects=None): +def append_obj(module, d, name, obj, autoload=False): + not_in_mod = not hasattr(obj, '__module__') or obj.__module__ != module.__name__ + if autoload: + # check needed for module global built-ins (int, str, dict,..) + if name.startswith('__') and not_in_mod: + return False + else: + if not_in_mod: + return False + + key = (module.__name__, name) + try: + d.setdefault(key, []).append(weakref.ref(obj)) + except TypeError: + pass + return True + + +def superreload(module, reload=reload, old_objects=None, shell=None): """Enhanced version of the builtin reload function. superreload remembers objects previously in the module, and @@ -371,7 +402,7 @@ def superreload(module, reload=reload, old_objects=None): # collect old objects in the module for name, obj in list(module.__dict__.items()): - if not hasattr(obj, '__module__') or obj.__module__ != module.__name__: + if not append_obj(module, old_objects, name, obj): continue key = (module.__name__, name) try: @@ -400,7 +431,15 @@ def superreload(module, reload=reload, old_objects=None): # iterate over all objects and update functions & classes for name, new_obj in list(module.__dict__.items()): key = (module.__name__, name) - if key not in old_objects: continue + if key not in old_objects: + # here 'shell' acts both as a flag and as an output var + if ( + shell is None or + name == 'Enum' or + not append_obj(module, old_objects, name, new_obj, True) + ): + continue + shell.user_ns[name] = new_obj new_refs = [] for old_ref in old_objects[key]: @@ -426,8 +465,9 @@ from IPython.core.magic import Magics, magics_class, line_magic class AutoreloadMagics(Magics): def __init__(self, *a, **kw): super(AutoreloadMagics, self).__init__(*a, **kw) - self._reloader = ModuleReloader() + self._reloader = ModuleReloader(self.shell) self._reloader.check_all = False + self._reloader.autoload_obj = False self.loaded_modules = set(sys.modules) @line_magic @@ -485,6 +525,11 @@ class AutoreloadMagics(Magics): elif parameter_s == '2': self._reloader.check_all = True self._reloader.enabled = True + self._reloader.enabled = True + elif parameter_s == '3': + self._reloader.check_all = True + self._reloader.enabled = True + self._reloader.autoload_obj = True @line_magic def aimport(self, parameter_s='', stream=None): diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index e81bf22..141307c 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -252,6 +252,89 @@ class TestAutoreload(Fixture): with nt.assert_raises(AttributeError): self.shell.run_code("{object_name}.toto".format(object_name=object_name)) + def test_autoload_newly_added_objects(self): + self.shell.magic_autoreload("3") + mod_code = """ + def func1(): pass + """ + mod_name, mod_fn = self.new_module(textwrap.dedent(mod_code)) + self.shell.run_code(f"from {mod_name} import *") + self.shell.run_code("func1()") + with nt.assert_raises(NameError): + self.shell.run_code('func2()') + with nt.assert_raises(NameError): + self.shell.run_code('t = Test()') + with nt.assert_raises(NameError): + self.shell.run_code('number') + + # ----------- TEST NEW OBJ LOADED -------------------------- + + new_code = """ + def func1(): pass + def func2(): pass + class Test: pass + number = 0 + from enum import Enum + class TestEnum(Enum): + A = 'a' + """ + self.write_file(mod_fn, textwrap.dedent(new_code)) + + # test function now exists in shell's namespace namespace + self.shell.run_code("func2()") + # test function now exists in module's dict + self.shell.run_code(f"import sys; sys.modules['{mod_name}'].func2()") + # test class now exists + self.shell.run_code("t = Test()") + # test global built-in var now exists + self.shell.run_code('number') + # test the enumerations gets loaded succesfully + self.shell.run_code("TestEnum.A") + + # ----------- TEST NEW OBJ CAN BE CHANGED -------------------- + + new_code = """ + def func1(): return 'changed' + def func2(): return 'changed' + class Test: + def new_func(self): + return 'changed' + number = 1 + from enum import Enum + class TestEnum(Enum): + A = 'a' + B = 'added' + """ + self.write_file(mod_fn, textwrap.dedent(new_code)) + self.shell.run_code("assert func1() == 'changed'") + self.shell.run_code("assert func2() == 'changed'") + self.shell.run_code("t = Test(); assert t.new_func() == 'changed'") + self.shell.run_code("assert number == 1") + self.shell.run_code("assert TestEnum.B.value == 'added'") + + # ----------- TEST IMPORT FROM MODULE -------------------------- + + new_mod_code = ''' + from enum import Enum + class Ext(Enum): + A = 'ext' + def ext_func(): + return 'ext' + class ExtTest: + def meth(self): + return 'ext' + ext_int = 2 + ''' + new_mod_name, new_mod_fn = self.new_module(textwrap.dedent(new_mod_code)) + current_mod_code = f''' + from {new_mod_name} import * + ''' + self.write_file(mod_fn, textwrap.dedent(current_mod_code)) + self.shell.run_code("assert Ext.A.value == 'ext'") + self.shell.run_code("assert ext_func() == 'ext'") + self.shell.run_code("t = ExtTest(); assert t.meth() == 'ext'") + self.shell.run_code("assert ext_int == 2") + def _check_smoketest(self, use_aimport=True): """ Functional test for the automatic reloader using either diff --git a/docs/source/whatsnew/pr/autoreload-option-3-feature.rst b/docs/source/whatsnew/pr/autoreload-option-3-feature.rst new file mode 100644 index 0000000..1adc444 --- /dev/null +++ b/docs/source/whatsnew/pr/autoreload-option-3-feature.rst @@ -0,0 +1,14 @@ +Autoreload 3 feature +==================== + +Example: When an IPython session is ran with the 'autoreload' extension loaded, +you will now have the option '3' to select which means the following: + + 1. replicate all functionality from option 2 + 2. autoload all new funcs/classes/enums/globals from the module when they're added + 3. autoload all newly imported funcs/classes/enums/globals from external modules + +Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload`` + +For more information please see unit test - + extensions/tests/test_autoreload.py : 'test_autoload_newly_added_objects'