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 | 48 | Reload all modules (except those excluded by ``%aimport``) every |
|
49 | 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 | 57 | ``%aimport`` |
|
52 | 58 | |
|
53 | 59 | List modules which are to be automatically imported or not to be imported. |
@@ -131,7 +137,10 b' class ModuleReloader(object):' | |||
|
131 | 137 | check_all = True |
|
132 | 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 | 144 | # Modules that failed to reload: {module: mtime-on-failed-reload, ...} |
|
136 | 145 | self.failed = {} |
|
137 | 146 | # Modules specially marked as autoreloadable. |
@@ -142,6 +151,7 b' class ModuleReloader(object):' | |||
|
142 | 151 | self.old_objects = {} |
|
143 | 152 | # Module modification timestamps |
|
144 | 153 | self.modules_mtimes = {} |
|
154 | self.shell = shell | |
|
145 | 155 | |
|
146 | 156 | # Cache module modification times |
|
147 | 157 | self.check(check_all=True, do_reload=False) |
@@ -242,7 +252,10 b' class ModuleReloader(object):' | |||
|
242 | 252 | # If we've reached this point, we should try to reload the module |
|
243 | 253 | if do_reload: |
|
244 | 254 | try: |
|
245 |
|
|
|
255 | if self.autoload_obj: | |
|
256 | superreload(m, reload, self.old_objects, self.shell) | |
|
257 | else: | |
|
258 | superreload(m, reload, self.old_objects) | |
|
246 | 259 | if py_filename in self.failed: |
|
247 | 260 | del self.failed[py_filename] |
|
248 | 261 | except: |
@@ -356,7 +369,25 b' class StrongRef(object):' | |||
|
356 | 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 | 391 | """Enhanced version of the builtin reload function. |
|
361 | 392 | |
|
362 | 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 | 403 | # collect old objects in the module |
|
373 | 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 | 406 | continue |
|
376 | 407 | key = (module.__name__, name) |
|
377 | 408 | try: |
@@ -400,7 +431,15 b' def superreload(module, reload=reload, old_objects=None):' | |||
|
400 | 431 | # iterate over all objects and update functions & classes |
|
401 | 432 | for name, new_obj in list(module.__dict__.items()): |
|
402 | 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 | 444 | new_refs = [] |
|
406 | 445 | for old_ref in old_objects[key]: |
@@ -426,8 +465,9 b' from IPython.core.magic import Magics, magics_class, line_magic' | |||
|
426 | 465 | class AutoreloadMagics(Magics): |
|
427 | 466 | def __init__(self, *a, **kw): |
|
428 | 467 | super(AutoreloadMagics, self).__init__(*a, **kw) |
|
429 | self._reloader = ModuleReloader() | |
|
468 | self._reloader = ModuleReloader(self.shell) | |
|
430 | 469 | self._reloader.check_all = False |
|
470 | self._reloader.autoload_obj = False | |
|
431 | 471 | self.loaded_modules = set(sys.modules) |
|
432 | 472 | |
|
433 | 473 | @line_magic |
@@ -485,6 +525,11 b' class AutoreloadMagics(Magics):' | |||
|
485 | 525 | elif parameter_s == '2': |
|
486 | 526 | self._reloader.check_all = True |
|
487 | 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 | 534 | @line_magic |
|
490 | 535 | def aimport(self, parameter_s='', stream=None): |
@@ -252,6 +252,89 b' class TestAutoreload(Fixture):' | |||
|
252 | 252 | with nt.assert_raises(AttributeError): |
|
253 | 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 | 338 | def _check_smoketest(self, use_aimport=True): |
|
256 | 339 | """ |
|
257 | 340 | Functional test for the automatic reloader using either |
General Comments 0
You need to be logged in to leave comments.
Login now