Show More
@@ -0,0 +1,26 b'' | |||||
|
1 | Autoreload verbosity | |||
|
2 | ==================== | |||
|
3 | ||||
|
4 | We introduce more descriptive names for the ``%autoreload`` parameter: | |||
|
5 | ||||
|
6 | - ``%autoreload now`` (also ``%autoreload``) - perform autoreload immediately. | |||
|
7 | - ``%autoreload off`` (also ``%autoreload 0``) - turn off autoreload. | |||
|
8 | - ``%autoreload explicit`` (also ``%autoreload 1``) - turn on autoreload only for modules | |||
|
9 | whitelisted by ``%aimport`` statements. | |||
|
10 | - ``%autoreload all`` (also ``%autoreload 2``) - turn on autoreload for all modules except those | |||
|
11 | blacklisted by ``%aimport`` statements. | |||
|
12 | - ``%autoreload complete`` (also ``%autoreload 3``) - all the fatures of ``all`` but also adding new | |||
|
13 | objects from the imported modules (see | |||
|
14 | IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects). | |||
|
15 | ||||
|
16 | The original designations (e.g. "2") still work, and these new ones are case-insensitive. | |||
|
17 | ||||
|
18 | Additionally, the option ``--print`` or ``-p`` can be added to the line to print the names of | |||
|
19 | modules being reloaded. Similarly, ``--log`` or ``-l`` will output the names to the logger at INFO | |||
|
20 | level. Both can be used simultaneously. | |||
|
21 | ||||
|
22 | The parsing logic for ``%aimport`` is now improved such that modules can be whitelisted and | |||
|
23 | blacklisted in the same line, e.g. it's now possible to call ``%aimport os, -math`` to include | |||
|
24 | ``os`` for ``%autoreload explicit`` and exclude ``math`` for modes ``all`` and ``complete``. | |||
|
25 | ||||
|
26 |
@@ -29,29 +29,33 b' Usage' | |||||
29 |
|
29 | |||
30 | The following magic commands are provided: |
|
30 | The following magic commands are provided: | |
31 |
|
31 | |||
32 | ``%autoreload`` |
|
32 | ``%autoreload``, ``%autoreload now`` | |
33 |
|
33 | |||
34 | Reload all modules (except those excluded by ``%aimport``) |
|
34 | Reload all modules (except those excluded by ``%aimport``) | |
35 | automatically now. |
|
35 | automatically now. | |
36 |
|
36 | |||
37 | ``%autoreload 0`` |
|
37 | ``%autoreload 0``, ``%autoreload off`` | |
38 |
|
38 | |||
39 | Disable automatic reloading. |
|
39 | Disable automatic reloading. | |
40 |
|
40 | |||
41 | ``%autoreload 1`` |
|
41 | ``%autoreload 1``, ``%autoreload explicit`` | |
42 |
|
42 | |||
43 | Reload all modules imported with ``%aimport`` every time before |
|
43 | Reload all modules imported with ``%aimport`` every time before | |
44 | executing the Python code typed. |
|
44 | executing the Python code typed. | |
45 |
|
45 | |||
46 | ``%autoreload 2`` |
|
46 | ``%autoreload 2``, ``%autoreload all`` | |
47 |
|
47 | |||
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`` |
|
51 | ``%autoreload 3``, ``%autoreload complete`` | |
52 |
|
52 | |||
53 | Reload all modules AND autoload newly added objects |
|
53 | Same as 2/all, but also adds any new objects in the module. See | |
54 | every time before executing the Python code typed. |
|
54 | unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects | |
|
55 | ||||
|
56 | Adding ``--print`` or ``-p`` to the ``%autoreload`` line will print autoreload activity to | |||
|
57 | standard out. ``--log`` or ``-l`` will do it to the log at INFO level; both can be used | |||
|
58 | simultaneously. | |||
55 |
|
59 | |||
56 | ``%aimport`` |
|
60 | ``%aimport`` | |
57 |
|
61 | |||
@@ -101,6 +105,9 b' Some of the known remaining caveats are:' | |||||
101 | - Reloading a module, or importing the same module by a different name, creates new Enums. These may look the same, but are not. |
|
105 | - Reloading a module, or importing the same module by a different name, creates new Enums. These may look the same, but are not. | |
102 | """ |
|
106 | """ | |
103 |
|
107 | |||
|
108 | from IPython.core import magic_arguments | |||
|
109 | from IPython.core.magic import Magics, magics_class, line_magic | |||
|
110 | ||||
104 | __skip_doctest__ = True |
|
111 | __skip_doctest__ = True | |
105 |
|
112 | |||
106 | # ----------------------------------------------------------------------------- |
|
113 | # ----------------------------------------------------------------------------- | |
@@ -125,6 +132,7 b' import traceback' | |||||
125 | import types |
|
132 | import types | |
126 | import weakref |
|
133 | import weakref | |
127 | import gc |
|
134 | import gc | |
|
135 | import logging | |||
128 | from importlib import import_module, reload |
|
136 | from importlib import import_module, reload | |
129 | from importlib.util import source_from_cache |
|
137 | from importlib.util import source_from_cache | |
130 |
|
138 | |||
@@ -156,6 +164,9 b' class ModuleReloader:' | |||||
156 | self.modules_mtimes = {} |
|
164 | self.modules_mtimes = {} | |
157 | self.shell = shell |
|
165 | self.shell = shell | |
158 |
|
166 | |||
|
167 | # Reporting callable for verbosity | |||
|
168 | self._report = lambda msg: None # by default, be quiet. | |||
|
169 | ||||
159 | # Cache module modification times |
|
170 | # Cache module modification times | |
160 | self.check(check_all=True, do_reload=False) |
|
171 | self.check(check_all=True, do_reload=False) | |
161 |
|
172 | |||
@@ -254,6 +265,7 b' class ModuleReloader:' | |||||
254 |
|
265 | |||
255 | # If we've reached this point, we should try to reload the module |
|
266 | # If we've reached this point, we should try to reload the module | |
256 | if do_reload: |
|
267 | if do_reload: | |
|
268 | self._report(f"Reloading '{modname}'.") | |||
257 | try: |
|
269 | try: | |
258 | if self.autoload_obj: |
|
270 | if self.autoload_obj: | |
259 | superreload(m, reload, self.old_objects, self.shell) |
|
271 | superreload(m, reload, self.old_objects, self.shell) | |
@@ -495,8 +507,6 b' def superreload(module, reload=reload, old_objects=None, shell=None):' | |||||
495 | # IPython connectivity |
|
507 | # IPython connectivity | |
496 | # ------------------------------------------------------------------------------ |
|
508 | # ------------------------------------------------------------------------------ | |
497 |
|
509 | |||
498 | from IPython.core.magic import Magics, magics_class, line_magic |
|
|||
499 |
|
||||
500 |
|
510 | |||
501 | @magics_class |
|
511 | @magics_class | |
502 | class AutoreloadMagics(Magics): |
|
512 | class AutoreloadMagics(Magics): | |
@@ -508,24 +518,67 b' class AutoreloadMagics(Magics):' | |||||
508 | self.loaded_modules = set(sys.modules) |
|
518 | self.loaded_modules = set(sys.modules) | |
509 |
|
519 | |||
510 | @line_magic |
|
520 | @line_magic | |
511 | def autoreload(self, parameter_s=""): |
|
521 | @magic_arguments.magic_arguments() | |
|
522 | @magic_arguments.argument( | |||
|
523 | "mode", | |||
|
524 | type=str, | |||
|
525 | default="now", | |||
|
526 | nargs="?", | |||
|
527 | help="""blank or 'now' - Reload all modules (except those excluded by %%aimport) | |||
|
528 | automatically now. | |||
|
529 | ||||
|
530 | '0' or 'off' - Disable automatic reloading. | |||
|
531 | ||||
|
532 | '1' or 'explicit' - Reload only modules imported with %%aimport every | |||
|
533 | time before executing the Python code typed. | |||
|
534 | ||||
|
535 | '2' or 'all' - Reload all modules (except those excluded by %%aimport) | |||
|
536 | every time before executing the Python code typed. | |||
|
537 | ||||
|
538 | '3' or 'complete' - Same as 2/all, but also but also adds any new | |||
|
539 | objects in the module. | |||
|
540 | """, | |||
|
541 | ) | |||
|
542 | @magic_arguments.argument( | |||
|
543 | "-p", | |||
|
544 | "--print", | |||
|
545 | action="store_true", | |||
|
546 | default=False, | |||
|
547 | help="Show autoreload activity using `print` statements", | |||
|
548 | ) | |||
|
549 | @magic_arguments.argument( | |||
|
550 | "-l", | |||
|
551 | "--log", | |||
|
552 | action="store_true", | |||
|
553 | default=False, | |||
|
554 | help="Show autoreload activity using the logger", | |||
|
555 | ) | |||
|
556 | def autoreload(self, line=""): | |||
512 | r"""%autoreload => Reload modules automatically |
|
557 | r"""%autoreload => Reload modules automatically | |
513 |
|
558 | |||
514 | %autoreload |
|
559 | %autoreload or %autoreload now | |
515 | Reload all modules (except those excluded by %aimport) automatically |
|
560 | Reload all modules (except those excluded by %aimport) automatically | |
516 | now. |
|
561 | now. | |
517 |
|
562 | |||
518 | %autoreload 0 |
|
563 | %autoreload 0 or %autoreload off | |
519 | Disable automatic reloading. |
|
564 | Disable automatic reloading. | |
520 |
|
565 | |||
521 | %autoreload 1 |
|
566 | %autoreload 1 or %autoreload explicit | |
522 |
Reload |
|
567 | Reload only modules imported with %aimport every time before executing | |
523 | the Python code typed. |
|
568 | the Python code typed. | |
524 |
|
569 | |||
525 | %autoreload 2 |
|
570 | %autoreload 2 or %autoreload all | |
526 | Reload all modules (except those excluded by %aimport) every time |
|
571 | Reload all modules (except those excluded by %aimport) every time | |
527 | before executing the Python code typed. |
|
572 | before executing the Python code typed. | |
528 |
|
573 | |||
|
574 | %autoreload 3 or %autoreload complete | |||
|
575 | Same as 2/all, but also but also adds any new objects in the module. See | |||
|
576 | unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects | |||
|
577 | ||||
|
578 | The optional arguments --print and --log control display of autoreload activity. The default | |||
|
579 | is to act silently; --print (or -p) will print out the names of modules that are being | |||
|
580 | reloaded, and --log (or -l) outputs them to the log at INFO level. | |||
|
581 | ||||
529 | Reloading Python modules in a reliable way is in general |
|
582 | Reloading Python modules in a reliable way is in general | |
530 | difficult, and unexpected things may occur. %autoreload tries to |
|
583 | difficult, and unexpected things may occur. %autoreload tries to | |
531 | work around common pitfalls by replacing function code objects and |
|
584 | work around common pitfalls by replacing function code objects and | |
@@ -552,21 +605,47 b' class AutoreloadMagics(Magics):' | |||||
552 | autoreloaded. |
|
605 | autoreloaded. | |
553 |
|
606 | |||
554 | """ |
|
607 | """ | |
555 | if parameter_s == "": |
|
608 | args = magic_arguments.parse_argstring(self.autoreload, line) | |
|
609 | mode = args.mode.lower() | |||
|
610 | ||||
|
611 | p = print | |||
|
612 | ||||
|
613 | logger = logging.getLogger("autoreload") | |||
|
614 | ||||
|
615 | l = logger.info | |||
|
616 | ||||
|
617 | def pl(msg): | |||
|
618 | p(msg) | |||
|
619 | l(msg) | |||
|
620 | ||||
|
621 | if args.print is False and args.log is False: | |||
|
622 | self._reloader._report = lambda msg: None | |||
|
623 | elif args.print is True: | |||
|
624 | if args.log is True: | |||
|
625 | self._reloader._report = pl | |||
|
626 | else: | |||
|
627 | self._reloader._report = p | |||
|
628 | elif args.log is True: | |||
|
629 | self._reloader._report = l | |||
|
630 | ||||
|
631 | if mode == "" or mode == "now": | |||
556 | self._reloader.check(True) |
|
632 | self._reloader.check(True) | |
557 |
elif |
|
633 | elif mode == "0" or mode == "off": | |
558 | self._reloader.enabled = False |
|
634 | self._reloader.enabled = False | |
559 | elif parameter_s == "1": |
|
635 | elif mode == "1" or mode == "explicit": | |
|
636 | self._reloader.enabled = True | |||
560 | self._reloader.check_all = False |
|
637 | self._reloader.check_all = False | |
|
638 | self._reloader.autoload_obj = False | |||
|
639 | elif mode == "2" or mode == "all": | |||
561 | self._reloader.enabled = True |
|
640 | self._reloader.enabled = True | |
562 | elif parameter_s == "2": |
|
|||
563 | self._reloader.check_all = True |
|
641 | self._reloader.check_all = True | |
|
642 | self._reloader.autoload_obj = False | |||
|
643 | elif mode == "3" or mode == "complete": | |||
564 | self._reloader.enabled = True |
|
644 | self._reloader.enabled = True | |
565 | self._reloader.enabled = True |
|
|||
566 | elif parameter_s == "3": |
|
|||
567 | self._reloader.check_all = True |
|
645 | self._reloader.check_all = True | |
568 | self._reloader.enabled = True |
|
|||
569 | self._reloader.autoload_obj = True |
|
646 | self._reloader.autoload_obj = True | |
|
647 | else: | |||
|
648 | raise ValueError(f'Unrecognized autoreload mode "{mode}".') | |||
570 |
|
649 | |||
571 | @line_magic |
|
650 | @line_magic | |
572 | def aimport(self, parameter_s="", stream=None): |
|
651 | def aimport(self, parameter_s="", stream=None): | |
@@ -576,13 +655,14 b' class AutoreloadMagics(Magics):' | |||||
576 | List modules to automatically import and not to import. |
|
655 | List modules to automatically import and not to import. | |
577 |
|
656 | |||
578 | %aimport foo |
|
657 | %aimport foo | |
579 |
Import module 'foo' and mark it to be autoreloaded for %autoreload |
|
658 | Import module 'foo' and mark it to be autoreloaded for %autoreload explicit | |
580 |
|
659 | |||
581 | %aimport foo, bar |
|
660 | %aimport foo, bar | |
582 |
Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload |
|
661 | Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload explicit | |
583 |
|
662 | |||
584 | %aimport -foo |
|
663 | %aimport -foo, bar | |
585 |
Mark module 'foo' to not be autoreloaded for %autoreload |
|
664 | Mark module 'foo' to not be autoreloaded for %autoreload explicit, all, or complete, and 'bar' | |
|
665 | to be autoreloaded for mode explicit. | |||
586 | """ |
|
666 | """ | |
587 | modname = parameter_s |
|
667 | modname = parameter_s | |
588 | if not modname: |
|
668 | if not modname: | |
@@ -595,11 +675,12 b' class AutoreloadMagics(Magics):' | |||||
595 | else: |
|
675 | else: | |
596 | stream.write("Modules to reload:\n%s\n" % " ".join(to_reload)) |
|
676 | stream.write("Modules to reload:\n%s\n" % " ".join(to_reload)) | |
597 | stream.write("\nModules to skip:\n%s\n" % " ".join(to_skip)) |
|
677 | stream.write("\nModules to skip:\n%s\n" % " ".join(to_skip)) | |
598 | elif modname.startswith("-"): |
|
|||
599 | modname = modname[1:] |
|
|||
600 | self._reloader.mark_module_skipped(modname) |
|
|||
601 | else: |
|
678 | else: | |
602 | for _module in [_.strip() for _ in modname.split(",")]: |
|
679 | for _module in [_.strip() for _ in modname.split(",")]: | |
|
680 | if _module.startswith("-"): | |||
|
681 | _module = _module[1:].strip() | |||
|
682 | self._reloader.mark_module_skipped(_module) | |||
|
683 | else: | |||
603 | top_module, top_name = self._reloader.aimport_module(_module) |
|
684 | top_module, top_name = self._reloader.aimport_module(_module) | |
604 |
|
685 | |||
605 | # Inject module to user namespace |
|
686 | # Inject module to user namespace |
@@ -22,6 +22,7 b' import shutil' | |||||
22 | import random |
|
22 | import random | |
23 | import time |
|
23 | import time | |
24 | from io import StringIO |
|
24 | from io import StringIO | |
|
25 | from dataclasses import dataclass | |||
25 |
|
26 | |||
26 | import IPython.testing.tools as tt |
|
27 | import IPython.testing.tools as tt | |
27 |
|
28 | |||
@@ -310,6 +311,7 b' class TestAutoreload(Fixture):' | |||||
310 | self.shell.run_code("pass") # trigger another reload |
|
311 | self.shell.run_code("pass") # trigger another reload | |
311 |
|
312 | |||
312 | def test_autoload_newly_added_objects(self): |
|
313 | def test_autoload_newly_added_objects(self): | |
|
314 | # All of these fail with %autoreload 2 | |||
313 | self.shell.magic_autoreload("3") |
|
315 | self.shell.magic_autoreload("3") | |
314 | mod_code = """ |
|
316 | mod_code = """ | |
315 | def func1(): pass |
|
317 | def func1(): pass | |
@@ -393,6 +395,95 b' class TestAutoreload(Fixture):' | |||||
393 | self.shell.run_code("t = ExtTest(); assert t.meth() == 'ext'") |
|
395 | self.shell.run_code("t = ExtTest(); assert t.meth() == 'ext'") | |
394 | self.shell.run_code("assert ext_int == 2") |
|
396 | self.shell.run_code("assert ext_int == 2") | |
395 |
|
397 | |||
|
398 | def test_verbose_names(self): | |||
|
399 | # Asserts correspondense between original mode names and their verbose equivalents. | |||
|
400 | @dataclass | |||
|
401 | class AutoreloadSettings: | |||
|
402 | check_all: bool | |||
|
403 | enabled: bool | |||
|
404 | autoload_obj: bool | |||
|
405 | ||||
|
406 | def gather_settings(mode): | |||
|
407 | self.shell.magic_autoreload(mode) | |||
|
408 | module_reloader = self.shell.auto_magics._reloader | |||
|
409 | return AutoreloadSettings( | |||
|
410 | module_reloader.check_all, | |||
|
411 | module_reloader.enabled, | |||
|
412 | module_reloader.autoload_obj, | |||
|
413 | ) | |||
|
414 | ||||
|
415 | assert gather_settings("0") == gather_settings("off") | |||
|
416 | assert gather_settings("0") == gather_settings("OFF") # Case insensitive | |||
|
417 | assert gather_settings("1") == gather_settings("explicit") | |||
|
418 | assert gather_settings("2") == gather_settings("all") | |||
|
419 | assert gather_settings("3") == gather_settings("complete") | |||
|
420 | ||||
|
421 | # And an invalid mode name raises an exception. | |||
|
422 | with self.assertRaises(ValueError): | |||
|
423 | self.shell.magic_autoreload("4") | |||
|
424 | ||||
|
425 | def test_aimport_parsing(self): | |||
|
426 | # Modules can be included or excluded all in one line. | |||
|
427 | module_reloader = self.shell.auto_magics._reloader | |||
|
428 | self.shell.magic_aimport("os") # import and mark `os` for auto-reload. | |||
|
429 | assert module_reloader.modules["os"] is True | |||
|
430 | assert "os" not in module_reloader.skip_modules.keys() | |||
|
431 | ||||
|
432 | self.shell.magic_aimport("-math") # forbid autoreloading of `math` | |||
|
433 | assert module_reloader.skip_modules["math"] is True | |||
|
434 | assert "math" not in module_reloader.modules.keys() | |||
|
435 | ||||
|
436 | self.shell.magic_aimport( | |||
|
437 | "-os, math" | |||
|
438 | ) # Can do this all in one line; wasn't possible before. | |||
|
439 | assert module_reloader.modules["math"] is True | |||
|
440 | assert "math" not in module_reloader.skip_modules.keys() | |||
|
441 | assert module_reloader.skip_modules["os"] is True | |||
|
442 | assert "os" not in module_reloader.modules.keys() | |||
|
443 | ||||
|
444 | def test_autoreload_output(self): | |||
|
445 | self.shell.magic_autoreload("complete") | |||
|
446 | mod_code = """ | |||
|
447 | def func1(): pass | |||
|
448 | """ | |||
|
449 | mod_name, mod_fn = self.new_module(mod_code) | |||
|
450 | self.shell.run_code(f"import {mod_name}") | |||
|
451 | with tt.AssertPrints("", channel="stdout"): # no output; this is default | |||
|
452 | self.shell.run_code("pass") | |||
|
453 | ||||
|
454 | self.shell.magic_autoreload("complete --print") | |||
|
455 | self.write_file(mod_fn, mod_code) # "modify" the module | |||
|
456 | with tt.AssertPrints( | |||
|
457 | f"Reloading '{mod_name}'.", channel="stdout" | |||
|
458 | ): # see something printed out | |||
|
459 | self.shell.run_code("pass") | |||
|
460 | ||||
|
461 | self.shell.magic_autoreload("complete -p") | |||
|
462 | self.write_file(mod_fn, mod_code) # "modify" the module | |||
|
463 | with tt.AssertPrints( | |||
|
464 | f"Reloading '{mod_name}'.", channel="stdout" | |||
|
465 | ): # see something printed out | |||
|
466 | self.shell.run_code("pass") | |||
|
467 | ||||
|
468 | self.shell.magic_autoreload("complete --print --log") | |||
|
469 | self.write_file(mod_fn, mod_code) # "modify" the module | |||
|
470 | with tt.AssertPrints( | |||
|
471 | f"Reloading '{mod_name}'.", channel="stdout" | |||
|
472 | ): # see something printed out | |||
|
473 | self.shell.run_code("pass") | |||
|
474 | ||||
|
475 | self.shell.magic_autoreload("complete --print --log") | |||
|
476 | self.write_file(mod_fn, mod_code) # "modify" the module | |||
|
477 | with self.assertLogs(logger="autoreload") as lo: # see something printed out | |||
|
478 | self.shell.run_code("pass") | |||
|
479 | assert lo.output == [f"INFO:autoreload:Reloading '{mod_name}'."] | |||
|
480 | ||||
|
481 | self.shell.magic_autoreload("complete -l") | |||
|
482 | self.write_file(mod_fn, mod_code) # "modify" the module | |||
|
483 | with self.assertLogs(logger="autoreload") as lo: # see something printed out | |||
|
484 | self.shell.run_code("pass") | |||
|
485 | assert lo.output == [f"INFO:autoreload:Reloading '{mod_name}'."] | |||
|
486 | ||||
396 | def _check_smoketest(self, use_aimport=True): |
|
487 | def _check_smoketest(self, use_aimport=True): | |
397 | """ |
|
488 | """ | |
398 | Functional test for the automatic reloader using either |
|
489 | Functional test for the automatic reloader using either |
General Comments 0
You need to be logged in to leave comments.
Login now