##// END OF EJS Templates
Backport PR #13506: Allow to configure lazy loadable magics.
Matthias Bussonnier -
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.find_line_magic(magic_name)
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.find_cell_magic(magic_name)
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 ip.magic('cpaste')
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 : txt
85 ip.hooks.clipboard_get = lambda: txt
85 ip.magic('paste '+flags)
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