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: |
|
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