##// 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 631 def __init__(self, ipython_dir=None, profile_dir=None,
632 632 user_module=None, user_ns=None,
633 633 custom_exceptions=((), None), **kwargs):
634
635 634 # This is where traits with a config_key argument are updated
636 635 # from the values on config.
637 636 super(InteractiveShell, self).__init__(**kwargs)
@@ -1787,7 +1786,7 b' class InteractiveShell(SingletonConfigurable):'
1787 1786 formatter,
1788 1787 info,
1789 1788 enable_html_pager=self.enable_html_pager,
1790 **kw
1789 **kw,
1791 1790 )
1792 1791 else:
1793 1792 pmethod(info.obj, oname)
@@ -2331,7 +2330,34 b' class InteractiveShell(SingletonConfigurable):'
2331 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 2361 """Execute the given line magic.
2336 2362
2337 2363 Parameters
@@ -2346,7 +2372,12 b' class InteractiveShell(SingletonConfigurable):'
2346 2372 If run_line_magic() is called from magic() then _stack_depth=2.
2347 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 2381 if fn is None:
2351 2382 cm = self.find_cell_magic(magic_name)
2352 2383 etpl = "Line magic function `%%%s` not found%s."
@@ -2399,7 +2430,7 b' class InteractiveShell(SingletonConfigurable):'
2399 2430 cell : str
2400 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 2434 if fn is None:
2404 2435 lm = self.find_line_magic(magic_name)
2405 2436 etpl = "Cell magic `%%{0}` not found{1}."
@@ -310,6 +310,34 b' class MagicsManager(Configurable):'
310 310 # holding the actual callable object as value. This is the dict used for
311 311 # magic function dispatch
312 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 342 # A registry of the original objects that we've been given holding magics.
315 343 registry = Dict()
@@ -374,6 +402,24 b' class MagicsManager(Configurable):'
374 402 docs[m_type] = m_docs
375 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 423 def register(self, *magic_objects):
378 424 """Register one or more instances of Magics.
379 425
@@ -32,8 +32,15 b' from IPython.utils.io import capture_output'
32 32 from IPython.utils.tempdir import (TemporaryDirectory,
33 33 TemporaryWorkingDirectory)
34 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 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 45 @magic.magics_class
39 46 class DummyMagics(magic.Magics): pass
@@ -1236,3 +1243,44 b' def test_timeit_arguments():'
1236 1243 # 3.7 optimize no-op statement like above out, and complain there is
1237 1244 # nothing in the for loop.
1238 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 14 import nose.tools as nt
15 15
16 16 from IPython.testing import tools as tt
17
18 17 #-----------------------------------------------------------------------------
19 18 # Test functions begin
20 19 #-----------------------------------------------------------------------------
21 20
21
22 22 def check_cpaste(code, should_fail=False):
23 23 """Execute code via 'cpaste' and ensure it was executed, unless
24 24 should_fail is set.
@@ -39,7 +39,7 b' def check_cpaste(code, should_fail=False):'
39 39 try:
40 40 context = tt.AssertPrints if should_fail else tt.AssertNotPrints
41 41 with context("Traceback (most recent call last)"):
42 ip.magic('cpaste')
42 ip.run_line_magic("cpaste", "")
43 43
44 44 if not should_fail:
45 45 assert ip.user_ns['code_ran'], "%r failed" % code
@@ -76,13 +76,14 b' def test_cpaste():'
76 76 check_cpaste(code, should_fail=True)
77 77
78 78
79
79 80 class PasteTestCase(TestCase):
80 81 """Multiple tests for clipboard pasting"""
81 82
82 83 def paste(self, txt, flags='-q'):
83 84 """Paste input text, by default in quiet mode"""
84 ip.hooks.clipboard_get = lambda : txt
85 ip.magic('paste '+flags)
85 ip.hooks.clipboard_get = lambda: txt
86 ip.run_line_magic("paste", flags)
86 87
87 88 def setUp(self):
88 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 25 from IPython.core.application import (
26 26 ProfileDir, BaseIPythonApplication, base_flags, base_aliases
27 27 )
28 from IPython.core.magic import MagicsManager
28 29 from IPython.core.magics import (
29 30 ScriptMagics, LoggingMagics
30 31 )
@@ -200,6 +201,7 b' class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):'
200 201 self.__class__, # it will also affect subclasses (e.g. QtConsole)
201 202 TerminalInteractiveShell,
202 203 HistoryManager,
204 MagicsManager,
203 205 ProfileDir,
204 206 PlainTextFormatter,
205 207 IPCompleter,
@@ -10,6 +10,24 b' IPython 7.31.1 (CVE-2022-21699)'
10 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 31 .. _version 7.31:
14 32
15 33 IPython 7.31
General Comments 0
You need to be logged in to leave comments. Login now