Show More
@@ -631,7 +631,6 b' class InteractiveShell(SingletonConfigurable):' | |||||
631 | def __init__(self, ipython_dir=None, profile_dir=None, |
|
631 | def __init__(self, ipython_dir=None, profile_dir=None, | |
632 | user_module=None, user_ns=None, |
|
632 | user_module=None, user_ns=None, | |
633 | custom_exceptions=((), None), **kwargs): |
|
633 | custom_exceptions=((), None), **kwargs): | |
634 |
|
||||
635 | # This is where traits with a config_key argument are updated |
|
634 | # This is where traits with a config_key argument are updated | |
636 | # from the values on config. |
|
635 | # from the values on config. | |
637 | super(InteractiveShell, self).__init__(**kwargs) |
|
636 | super(InteractiveShell, self).__init__(**kwargs) | |
@@ -1787,7 +1786,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
1787 | formatter, |
|
1786 | formatter, | |
1788 | info, |
|
1787 | info, | |
1789 | enable_html_pager=self.enable_html_pager, |
|
1788 | enable_html_pager=self.enable_html_pager, | |
1790 | **kw |
|
1789 | **kw, | |
1791 | ) |
|
1790 | ) | |
1792 | else: |
|
1791 | else: | |
1793 | pmethod(info.obj, oname) |
|
1792 | pmethod(info.obj, oname) | |
@@ -2331,7 +2330,34 b' class InteractiveShell(SingletonConfigurable):' | |||||
2331 | func, magic_kind=magic_kind, magic_name=magic_name |
|
2330 | func, magic_kind=magic_kind, magic_name=magic_name | |
2332 | ) |
|
2331 | ) | |
2333 |
|
2332 | |||
2334 | def run_line_magic(self, magic_name, line, _stack_depth=1): |
|
2333 | def _find_with_lazy_load(self, type_, magic_name: str): | |
|
2334 | """ | |||
|
2335 | Try to find a magic potentially lazy-loading it. | |||
|
2336 | ||||
|
2337 | Parameters | |||
|
2338 | ---------- | |||
|
2339 | ||||
|
2340 | type_: "line"|"cell" | |||
|
2341 | the type of magics we are trying to find/lazy load. | |||
|
2342 | magic_name: str | |||
|
2343 | The name of the magic we are trying to find/lazy load | |||
|
2344 | ||||
|
2345 | ||||
|
2346 | Note that this may have any side effects | |||
|
2347 | """ | |||
|
2348 | finder = {"line": self.find_line_magic, "cell": self.find_cell_magic}[type_] | |||
|
2349 | fn = finder(magic_name) | |||
|
2350 | if fn is not None: | |||
|
2351 | return fn | |||
|
2352 | lazy = self.magics_manager.lazy_magics.get(magic_name) | |||
|
2353 | if lazy is None: | |||
|
2354 | return None | |||
|
2355 | ||||
|
2356 | self.run_line_magic("load_ext", lazy) | |||
|
2357 | res = finder(magic_name) | |||
|
2358 | return res | |||
|
2359 | ||||
|
2360 | def run_line_magic(self, magic_name: str, line, _stack_depth=1): | |||
2335 | """Execute the given line magic. |
|
2361 | """Execute the given line magic. | |
2336 |
|
2362 | |||
2337 | Parameters |
|
2363 | Parameters | |
@@ -2346,7 +2372,12 b' class InteractiveShell(SingletonConfigurable):' | |||||
2346 | If run_line_magic() is called from magic() then _stack_depth=2. |
|
2372 | If run_line_magic() is called from magic() then _stack_depth=2. | |
2347 | This is added to ensure backward compatibility for use of 'get_ipython().magic()' |
|
2373 | This is added to ensure backward compatibility for use of 'get_ipython().magic()' | |
2348 | """ |
|
2374 | """ | |
2349 |
fn = self. |
|
2375 | fn = self._find_with_lazy_load("line", magic_name) | |
|
2376 | if fn is None: | |||
|
2377 | lazy = self.magics_manager.lazy_magics.get(magic_name) | |||
|
2378 | if lazy: | |||
|
2379 | self.run_line_magic("load_ext", lazy) | |||
|
2380 | fn = self.find_line_magic(magic_name) | |||
2350 | if fn is None: |
|
2381 | if fn is None: | |
2351 | cm = self.find_cell_magic(magic_name) |
|
2382 | cm = self.find_cell_magic(magic_name) | |
2352 | etpl = "Line magic function `%%%s` not found%s." |
|
2383 | etpl = "Line magic function `%%%s` not found%s." | |
@@ -2399,7 +2430,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
2399 | cell : str |
|
2430 | cell : str | |
2400 | The body of the cell as a (possibly multiline) string. |
|
2431 | The body of the cell as a (possibly multiline) string. | |
2401 | """ |
|
2432 | """ | |
2402 |
fn = self. |
|
2433 | fn = self._find_with_lazy_load("cell", magic_name) | |
2403 | if fn is None: |
|
2434 | if fn is None: | |
2404 | lm = self.find_line_magic(magic_name) |
|
2435 | lm = self.find_line_magic(magic_name) | |
2405 | etpl = "Cell magic `%%{0}` not found{1}." |
|
2436 | etpl = "Cell magic `%%{0}` not found{1}." |
@@ -310,6 +310,34 b' class MagicsManager(Configurable):' | |||||
310 | # holding the actual callable object as value. This is the dict used for |
|
310 | # holding the actual callable object as value. This is the dict used for | |
311 | # magic function dispatch |
|
311 | # magic function dispatch | |
312 | magics = Dict() |
|
312 | magics = Dict() | |
|
313 | lazy_magics = Dict( | |||
|
314 | help=""" | |||
|
315 | Mapping from magic names to modules to load. | |||
|
316 | ||||
|
317 | This can be used in IPython/IPykernel configuration to declare lazy magics | |||
|
318 | that will only be imported/registered on first use. | |||
|
319 | ||||
|
320 | For example:: | |||
|
321 | ||||
|
322 | c.MagicsManger.lazy_magics = { | |||
|
323 | "my_magic": "slow.to.import", | |||
|
324 | "my_other_magic": "also.slow", | |||
|
325 | } | |||
|
326 | ||||
|
327 | On first invocation of `%my_magic`, `%%my_magic`, `%%my_other_magic` or | |||
|
328 | `%%my_other_magic`, the corresponding module will be loaded as an ipython | |||
|
329 | extensions as if you had previously done `%load_ext ipython`. | |||
|
330 | ||||
|
331 | Magics names should be without percent(s) as magics can be both cell | |||
|
332 | and line magics. | |||
|
333 | ||||
|
334 | Lazy loading happen relatively late in execution process, and | |||
|
335 | complex extensions that manipulate Python/IPython internal state or global state | |||
|
336 | might not support lazy loading. | |||
|
337 | """ | |||
|
338 | ).tag( | |||
|
339 | config=True, | |||
|
340 | ) | |||
313 |
|
341 | |||
314 | # A registry of the original objects that we've been given holding magics. |
|
342 | # A registry of the original objects that we've been given holding magics. | |
315 | registry = Dict() |
|
343 | registry = Dict() | |
@@ -374,6 +402,24 b' class MagicsManager(Configurable):' | |||||
374 | docs[m_type] = m_docs |
|
402 | docs[m_type] = m_docs | |
375 | return docs |
|
403 | return docs | |
376 |
|
404 | |||
|
405 | def register_lazy(self, name: str, fully_qualified_name: str): | |||
|
406 | """ | |||
|
407 | Lazily register a magic via an extension. | |||
|
408 | ||||
|
409 | ||||
|
410 | Parameters | |||
|
411 | ---------- | |||
|
412 | name : str | |||
|
413 | Name of the magic you wish to register. | |||
|
414 | fully_qualified_name : | |||
|
415 | Fully qualified name of the module/submodule that should be loaded | |||
|
416 | as an extensions when the magic is first called. | |||
|
417 | It is assumed that loading this extensions will register the given | |||
|
418 | magic. | |||
|
419 | """ | |||
|
420 | ||||
|
421 | self.lazy_magics[name] = fully_qualified_name | |||
|
422 | ||||
377 | def register(self, *magic_objects): |
|
423 | def register(self, *magic_objects): | |
378 | """Register one or more instances of Magics. |
|
424 | """Register one or more instances of Magics. | |
379 |
|
425 |
@@ -32,8 +32,15 b' from IPython.utils.io import capture_output' | |||||
32 | from IPython.utils.tempdir import (TemporaryDirectory, |
|
32 | from IPython.utils.tempdir import (TemporaryDirectory, | |
33 | TemporaryWorkingDirectory) |
|
33 | TemporaryWorkingDirectory) | |
34 | from IPython.utils.process import find_cmd |
|
34 | from IPython.utils.process import find_cmd | |
|
35 | from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory | |||
|
36 | from IPython.utils.syspathcontext import prepended_to_syspath | |||
|
37 | ||||
35 | from .test_debugger import PdbTestInput |
|
38 | from .test_debugger import PdbTestInput | |
36 |
|
39 | |||
|
40 | from tempfile import NamedTemporaryFile | |||
|
41 | ||||
|
42 | import pytest | |||
|
43 | from pathlib import Path | |||
37 |
|
44 | |||
38 | @magic.magics_class |
|
45 | @magic.magics_class | |
39 | class DummyMagics(magic.Magics): pass |
|
46 | class DummyMagics(magic.Magics): pass | |
@@ -1236,3 +1243,44 b' def test_timeit_arguments():' | |||||
1236 | # 3.7 optimize no-op statement like above out, and complain there is |
|
1243 | # 3.7 optimize no-op statement like above out, and complain there is | |
1237 | # nothing in the for loop. |
|
1244 | # nothing in the for loop. | |
1238 | _ip.magic("timeit -n1 -r1 a=('#')") |
|
1245 | _ip.magic("timeit -n1 -r1 a=('#')") | |
|
1246 | ||||
|
1247 | ||||
|
1248 | MINIMAL_LAZY_MAGIC = """ | |||
|
1249 | from IPython.core.magic import ( | |||
|
1250 | Magics, | |||
|
1251 | magics_class, | |||
|
1252 | line_magic, | |||
|
1253 | cell_magic, | |||
|
1254 | ) | |||
|
1255 | ||||
|
1256 | ||||
|
1257 | @magics_class | |||
|
1258 | class LazyMagics(Magics): | |||
|
1259 | @line_magic | |||
|
1260 | def lazy_line(self, line): | |||
|
1261 | print("Lazy Line") | |||
|
1262 | ||||
|
1263 | @cell_magic | |||
|
1264 | def lazy_cell(self, line, cell): | |||
|
1265 | print("Lazy Cell") | |||
|
1266 | ||||
|
1267 | ||||
|
1268 | def load_ipython_extension(ipython): | |||
|
1269 | ipython.register_magics(LazyMagics) | |||
|
1270 | """ | |||
|
1271 | ||||
|
1272 | ||||
|
1273 | def test_lazy_magics(): | |||
|
1274 | with pytest.raises(UsageError): | |||
|
1275 | ip.run_line_magic("lazy_line", "") | |||
|
1276 | ||||
|
1277 | startdir = os.getcwd() | |||
|
1278 | ||||
|
1279 | with TemporaryDirectory() as tmpdir: | |||
|
1280 | with prepended_to_syspath(tmpdir): | |||
|
1281 | ptempdir = Path(tmpdir) | |||
|
1282 | tf = ptempdir / "lazy_magic_module.py" | |||
|
1283 | tf.write_text(MINIMAL_LAZY_MAGIC) | |||
|
1284 | ip.magics_manager.register_lazy("lazy_line", Path(tf.name).name[:-3]) | |||
|
1285 | with tt.AssertPrints("Lazy Line"): | |||
|
1286 | ip.run_line_magic("lazy_line", "") |
@@ -14,11 +14,11 b' from unittest import TestCase' | |||||
14 | import nose.tools as nt |
|
14 | import nose.tools as nt | |
15 |
|
15 | |||
16 | from IPython.testing import tools as tt |
|
16 | from IPython.testing import tools as tt | |
17 |
|
||||
18 | #----------------------------------------------------------------------------- |
|
17 | #----------------------------------------------------------------------------- | |
19 | # Test functions begin |
|
18 | # Test functions begin | |
20 | #----------------------------------------------------------------------------- |
|
19 | #----------------------------------------------------------------------------- | |
21 |
|
20 | |||
|
21 | ||||
22 | def check_cpaste(code, should_fail=False): |
|
22 | def check_cpaste(code, should_fail=False): | |
23 | """Execute code via 'cpaste' and ensure it was executed, unless |
|
23 | """Execute code via 'cpaste' and ensure it was executed, unless | |
24 | should_fail is set. |
|
24 | should_fail is set. | |
@@ -39,7 +39,7 b' def check_cpaste(code, should_fail=False):' | |||||
39 | try: |
|
39 | try: | |
40 | context = tt.AssertPrints if should_fail else tt.AssertNotPrints |
|
40 | context = tt.AssertPrints if should_fail else tt.AssertNotPrints | |
41 | with context("Traceback (most recent call last)"): |
|
41 | with context("Traceback (most recent call last)"): | |
42 |
|
|
42 | ip.run_line_magic("cpaste", "") | |
43 |
|
43 | |||
44 | if not should_fail: |
|
44 | if not should_fail: | |
45 | assert ip.user_ns['code_ran'], "%r failed" % code |
|
45 | assert ip.user_ns['code_ran'], "%r failed" % code | |
@@ -76,13 +76,14 b' def test_cpaste():' | |||||
76 | check_cpaste(code, should_fail=True) |
|
76 | check_cpaste(code, should_fail=True) | |
77 |
|
77 | |||
78 |
|
78 | |||
|
79 | ||||
79 | class PasteTestCase(TestCase): |
|
80 | class PasteTestCase(TestCase): | |
80 | """Multiple tests for clipboard pasting""" |
|
81 | """Multiple tests for clipboard pasting""" | |
81 |
|
82 | |||
82 | def paste(self, txt, flags='-q'): |
|
83 | def paste(self, txt, flags='-q'): | |
83 | """Paste input text, by default in quiet mode""" |
|
84 | """Paste input text, by default in quiet mode""" | |
84 |
ip.hooks.clipboard_get = lambda |
|
85 | ip.hooks.clipboard_get = lambda: txt | |
85 |
ip.magic( |
|
86 | ip.run_line_magic("paste", flags) | |
86 |
|
87 | |||
87 | def setUp(self): |
|
88 | def setUp(self): | |
88 | # Inject fake clipboard hook but save original so we can restore it later |
|
89 | # Inject fake clipboard hook but save original so we can restore it later |
@@ -25,6 +25,7 b' from IPython.core.history import HistoryManager' | |||||
25 | from IPython.core.application import ( |
|
25 | from IPython.core.application import ( | |
26 | ProfileDir, BaseIPythonApplication, base_flags, base_aliases |
|
26 | ProfileDir, BaseIPythonApplication, base_flags, base_aliases | |
27 | ) |
|
27 | ) | |
|
28 | from IPython.core.magic import MagicsManager | |||
28 | from IPython.core.magics import ( |
|
29 | from IPython.core.magics import ( | |
29 | ScriptMagics, LoggingMagics |
|
30 | ScriptMagics, LoggingMagics | |
30 | ) |
|
31 | ) | |
@@ -200,6 +201,7 b' class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):' | |||||
200 | self.__class__, # it will also affect subclasses (e.g. QtConsole) |
|
201 | self.__class__, # it will also affect subclasses (e.g. QtConsole) | |
201 | TerminalInteractiveShell, |
|
202 | TerminalInteractiveShell, | |
202 | HistoryManager, |
|
203 | HistoryManager, | |
|
204 | MagicsManager, | |||
203 | ProfileDir, |
|
205 | ProfileDir, | |
204 | PlainTextFormatter, |
|
206 | PlainTextFormatter, | |
205 | IPCompleter, |
|
207 | IPCompleter, |
@@ -10,6 +10,24 b' IPython 7.31.1 (CVE-2022-21699)' | |||||
10 | Fixed CVE-2022-21699, see IPython 8.0.1 release notes for informations. |
|
10 | Fixed CVE-2022-21699, see IPython 8.0.1 release notes for informations. | |
11 |
|
11 | |||
12 |
|
12 | |||
|
13 | .. _version 7.32: | |||
|
14 | ||||
|
15 | IPython 7.32 | |||
|
16 | ============ | |||
|
17 | ||||
|
18 | ||||
|
19 | The ability to configure magics to be lazily loaded has been added to IPython. | |||
|
20 | See the ``ipython --help-all`` section on ``MagicsManager.lazy_magic``. | |||
|
21 | One can now use:: | |||
|
22 | ||||
|
23 | c.MagicsManger.lazy_magics = { | |||
|
24 | "my_magic": "slow.to.import", | |||
|
25 | "my_other_magic": "also.slow", | |||
|
26 | } | |||
|
27 | ||||
|
28 | And on first use of ``%my_magic``, or corresponding cell magic, or other line magic, | |||
|
29 | the corresponding ``load_ext`` will be called just before trying to invoke the magic. | |||
|
30 | ||||
13 | .. _version 7.31: |
|
31 | .. _version 7.31: | |
14 |
|
32 | |||
15 | IPython 7.31 |
|
33 | IPython 7.31 |
General Comments 0
You need to be logged in to leave comments.
Login now