##// END OF EJS Templates
Add new '%autoreload 3' option...
Spas Kalaydzhisyki -
Show More
@@ -0,0 +1,14 b''
1 Autoreload 3 feature
2 ====================
3
4 Example: When an IPython session is ran with the 'autoreload' extension loaded,
5 you will now have the option '3' to select which means the following:
6
7 1. replicate all functionality from option 2
8 2. autoload all new funcs/classes/enums/globals from the module when they're added
9 3. autoload all newly imported funcs/classes/enums/globals from external modules
10
11 Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload``
12
13 For more information please see unit test -
14 extensions/tests/test_autoreload.py : 'test_autoload_newly_added_objects'
@@ -48,6 +48,12 b' The following magic commands are provided:'
48 Reload all modules (except those excluded by ``%aimport``) every
48 Reload all modules (except those excluded by ``%aimport``) every
49 time before executing the Python code typed.
49 time before executing the Python code typed.
50
50
51 ``%autoreload 3``
52
53 Reload all modules AND autoload newly added objects
54 (except those excluded by ``%aimport``)
55 every time before executing the Python code typed.
56
51 ``%aimport``
57 ``%aimport``
52
58
53 List modules which are to be automatically imported or not to be imported.
59 List modules which are to be automatically imported or not to be imported.
@@ -131,7 +137,10 b' class ModuleReloader(object):'
131 check_all = True
137 check_all = True
132 """Autoreload all modules, not just those listed in 'modules'"""
138 """Autoreload all modules, not just those listed in 'modules'"""
133
139
134 def __init__(self):
140 autoload_obj = False
141 """Autoreload all modules AND autoload all new objects"""
142
143 def __init__(self, shell=None):
135 # Modules that failed to reload: {module: mtime-on-failed-reload, ...}
144 # Modules that failed to reload: {module: mtime-on-failed-reload, ...}
136 self.failed = {}
145 self.failed = {}
137 # Modules specially marked as autoreloadable.
146 # Modules specially marked as autoreloadable.
@@ -142,6 +151,7 b' class ModuleReloader(object):'
142 self.old_objects = {}
151 self.old_objects = {}
143 # Module modification timestamps
152 # Module modification timestamps
144 self.modules_mtimes = {}
153 self.modules_mtimes = {}
154 self.shell = shell
145
155
146 # Cache module modification times
156 # Cache module modification times
147 self.check(check_all=True, do_reload=False)
157 self.check(check_all=True, do_reload=False)
@@ -242,6 +252,9 b' class ModuleReloader(object):'
242 # If we've reached this point, we should try to reload the module
252 # If we've reached this point, we should try to reload the module
243 if do_reload:
253 if do_reload:
244 try:
254 try:
255 if self.autoload_obj:
256 superreload(m, reload, self.old_objects, self.shell)
257 else:
245 superreload(m, reload, self.old_objects)
258 superreload(m, reload, self.old_objects)
246 if py_filename in self.failed:
259 if py_filename in self.failed:
247 del self.failed[py_filename]
260 del self.failed[py_filename]
@@ -356,7 +369,25 b' class StrongRef(object):'
356 return self.obj
369 return self.obj
357
370
358
371
359 def superreload(module, reload=reload, old_objects=None):
372 def append_obj(module, d, name, obj, autoload=False):
373 not_in_mod = not hasattr(obj, '__module__') or obj.__module__ != module.__name__
374 if autoload:
375 # check needed for module global built-ins (int, str, dict,..)
376 if name.startswith('__') and not_in_mod:
377 return False
378 else:
379 if not_in_mod:
380 return False
381
382 key = (module.__name__, name)
383 try:
384 d.setdefault(key, []).append(weakref.ref(obj))
385 except TypeError:
386 pass
387 return True
388
389
390 def superreload(module, reload=reload, old_objects=None, shell=None):
360 """Enhanced version of the builtin reload function.
391 """Enhanced version of the builtin reload function.
361
392
362 superreload remembers objects previously in the module, and
393 superreload remembers objects previously in the module, and
@@ -371,7 +402,7 b' def superreload(module, reload=reload, old_objects=None):'
371
402
372 # collect old objects in the module
403 # collect old objects in the module
373 for name, obj in list(module.__dict__.items()):
404 for name, obj in list(module.__dict__.items()):
374 if not hasattr(obj, '__module__') or obj.__module__ != module.__name__:
405 if not append_obj(module, old_objects, name, obj):
375 continue
406 continue
376 key = (module.__name__, name)
407 key = (module.__name__, name)
377 try:
408 try:
@@ -400,7 +431,15 b' def superreload(module, reload=reload, old_objects=None):'
400 # iterate over all objects and update functions & classes
431 # iterate over all objects and update functions & classes
401 for name, new_obj in list(module.__dict__.items()):
432 for name, new_obj in list(module.__dict__.items()):
402 key = (module.__name__, name)
433 key = (module.__name__, name)
403 if key not in old_objects: continue
434 if key not in old_objects:
435 # here 'shell' acts both as a flag and as an output var
436 if (
437 shell is None or
438 name == 'Enum' or
439 not append_obj(module, old_objects, name, new_obj, True)
440 ):
441 continue
442 shell.user_ns[name] = new_obj
404
443
405 new_refs = []
444 new_refs = []
406 for old_ref in old_objects[key]:
445 for old_ref in old_objects[key]:
@@ -426,8 +465,9 b' from IPython.core.magic import Magics, magics_class, line_magic'
426 class AutoreloadMagics(Magics):
465 class AutoreloadMagics(Magics):
427 def __init__(self, *a, **kw):
466 def __init__(self, *a, **kw):
428 super(AutoreloadMagics, self).__init__(*a, **kw)
467 super(AutoreloadMagics, self).__init__(*a, **kw)
429 self._reloader = ModuleReloader()
468 self._reloader = ModuleReloader(self.shell)
430 self._reloader.check_all = False
469 self._reloader.check_all = False
470 self._reloader.autoload_obj = False
431 self.loaded_modules = set(sys.modules)
471 self.loaded_modules = set(sys.modules)
432
472
433 @line_magic
473 @line_magic
@@ -485,6 +525,11 b' class AutoreloadMagics(Magics):'
485 elif parameter_s == '2':
525 elif parameter_s == '2':
486 self._reloader.check_all = True
526 self._reloader.check_all = True
487 self._reloader.enabled = True
527 self._reloader.enabled = True
528 self._reloader.enabled = True
529 elif parameter_s == '3':
530 self._reloader.check_all = True
531 self._reloader.enabled = True
532 self._reloader.autoload_obj = True
488
533
489 @line_magic
534 @line_magic
490 def aimport(self, parameter_s='', stream=None):
535 def aimport(self, parameter_s='', stream=None):
@@ -252,6 +252,89 b' class TestAutoreload(Fixture):'
252 with nt.assert_raises(AttributeError):
252 with nt.assert_raises(AttributeError):
253 self.shell.run_code("{object_name}.toto".format(object_name=object_name))
253 self.shell.run_code("{object_name}.toto".format(object_name=object_name))
254
254
255 def test_autoload_newly_added_objects(self):
256 self.shell.magic_autoreload("3")
257 mod_code = """
258 def func1(): pass
259 """
260 mod_name, mod_fn = self.new_module(textwrap.dedent(mod_code))
261 self.shell.run_code(f"from {mod_name} import *")
262 self.shell.run_code("func1()")
263 with nt.assert_raises(NameError):
264 self.shell.run_code('func2()')
265 with nt.assert_raises(NameError):
266 self.shell.run_code('t = Test()')
267 with nt.assert_raises(NameError):
268 self.shell.run_code('number')
269
270 # ----------- TEST NEW OBJ LOADED --------------------------
271
272 new_code = """
273 def func1(): pass
274 def func2(): pass
275 class Test: pass
276 number = 0
277 from enum import Enum
278 class TestEnum(Enum):
279 A = 'a'
280 """
281 self.write_file(mod_fn, textwrap.dedent(new_code))
282
283 # test function now exists in shell's namespace namespace
284 self.shell.run_code("func2()")
285 # test function now exists in module's dict
286 self.shell.run_code(f"import sys; sys.modules['{mod_name}'].func2()")
287 # test class now exists
288 self.shell.run_code("t = Test()")
289 # test global built-in var now exists
290 self.shell.run_code('number')
291 # test the enumerations gets loaded succesfully
292 self.shell.run_code("TestEnum.A")
293
294 # ----------- TEST NEW OBJ CAN BE CHANGED --------------------
295
296 new_code = """
297 def func1(): return 'changed'
298 def func2(): return 'changed'
299 class Test:
300 def new_func(self):
301 return 'changed'
302 number = 1
303 from enum import Enum
304 class TestEnum(Enum):
305 A = 'a'
306 B = 'added'
307 """
308 self.write_file(mod_fn, textwrap.dedent(new_code))
309 self.shell.run_code("assert func1() == 'changed'")
310 self.shell.run_code("assert func2() == 'changed'")
311 self.shell.run_code("t = Test(); assert t.new_func() == 'changed'")
312 self.shell.run_code("assert number == 1")
313 self.shell.run_code("assert TestEnum.B.value == 'added'")
314
315 # ----------- TEST IMPORT FROM MODULE --------------------------
316
317 new_mod_code = '''
318 from enum import Enum
319 class Ext(Enum):
320 A = 'ext'
321 def ext_func():
322 return 'ext'
323 class ExtTest:
324 def meth(self):
325 return 'ext'
326 ext_int = 2
327 '''
328 new_mod_name, new_mod_fn = self.new_module(textwrap.dedent(new_mod_code))
329 current_mod_code = f'''
330 from {new_mod_name} import *
331 '''
332 self.write_file(mod_fn, textwrap.dedent(current_mod_code))
333 self.shell.run_code("assert Ext.A.value == 'ext'")
334 self.shell.run_code("assert ext_func() == 'ext'")
335 self.shell.run_code("t = ExtTest(); assert t.meth() == 'ext'")
336 self.shell.run_code("assert ext_int == 2")
337
255 def _check_smoketest(self, use_aimport=True):
338 def _check_smoketest(self, use_aimport=True):
256 """
339 """
257 Functional test for the automatic reloader using either
340 Functional test for the automatic reloader using either
General Comments 0
You need to be logged in to leave comments. Login now