##// END OF EJS Templates
Allow to configure lazy loadable magics....
Matthias Bussonnier -
Show More
@@ -500,7 +500,6 b' class InteractiveShell(SingletonConfigurable):'
500 500 def __init__(self, ipython_dir=None, profile_dir=None,
501 501 user_module=None, user_ns=None,
502 502 custom_exceptions=((), None), **kwargs):
503
504 503 # This is where traits with a config_key argument are updated
505 504 # from the values on config.
506 505 super(InteractiveShell, self).__init__(**kwargs)
@@ -1643,7 +1642,7 b' class InteractiveShell(SingletonConfigurable):'
1643 1642 formatter,
1644 1643 info,
1645 1644 enable_html_pager=self.enable_html_pager,
1646 **kw
1645 **kw,
1647 1646 )
1648 1647 else:
1649 1648 pmethod(info.obj, oname)
@@ -2173,7 +2172,34 b' class InteractiveShell(SingletonConfigurable):'
2173 2172 func, magic_kind=magic_kind, magic_name=magic_name
2174 2173 )
2175 2174
2176 def run_line_magic(self, magic_name, line, _stack_depth=1):
2175 def _find_with_lazy_load(self, /, type_, magic_name: str):
2176 """
2177 Try to find a magic potentially lazy-loading it.
2178
2179 Parameters
2180 ----------
2181
2182 type_: "line"|"cell"
2183 the type of magics we are trying to find/lazy load.
2184 magic_name: str
2185 The name of the magic we are trying to find/lazy load
2186
2187
2188 Note that this may have any side effects
2189 """
2190 finder = {"line": self.find_line_magic, "cell": self.find_cell_magic}[type_]
2191 fn = finder(magic_name)
2192 if fn is not None:
2193 return fn
2194 lazy = self.magics_manager.lazy_magics.get(magic_name)
2195 if lazy is None:
2196 return None
2197
2198 self.run_line_magic("load_ext", lazy)
2199 res = finder(magic_name)
2200 return res
2201
2202 def run_line_magic(self, magic_name: str, line, _stack_depth=1):
2177 2203 """Execute the given line magic.
2178 2204
2179 2205 Parameters
@@ -2186,6 +2212,11 b' class InteractiveShell(SingletonConfigurable):'
2186 2212 If run_line_magic() is called from magic() then _stack_depth=2.
2187 2213 This is added to ensure backward compatibility for use of 'get_ipython().magic()'
2188 2214 """
2215 fn = self._find_with_lazy_load("line", magic_name)
2216 if fn is None:
2217 lazy = self.magics_manager.lazy_magics.get(magic_name)
2218 if lazy:
2219 self.run_line_magic("load_ext", lazy)
2189 2220 fn = self.find_line_magic(magic_name)
2190 2221 if fn is None:
2191 2222 cm = self.find_cell_magic(magic_name)
@@ -2237,7 +2268,7 b' class InteractiveShell(SingletonConfigurable):'
2237 2268 cell : str
2238 2269 The body of the cell as a (possibly multiline) string.
2239 2270 """
2240 fn = self.find_cell_magic(magic_name)
2271 fn = self._find_with_lazy_load("cell", magic_name)
2241 2272 if fn is None:
2242 2273 lm = self.find_line_magic(magic_name)
2243 2274 etpl = "Cell magic `%%{0}` not found{1}."
@@ -302,6 +302,34 b' class MagicsManager(Configurable):'
302 302 # holding the actual callable object as value. This is the dict used for
303 303 # magic function dispatch
304 304 magics = Dict()
305 lazy_magics = Dict(
306 help="""
307 Mapping from magic names to modules to load.
308
309 This can be used in IPython/IPykernel configuration to declare lazy magics
310 that will only be imported/registered on first use.
311
312 For example::
313
314 c.MagicsManger.lazy_magics = {
315 "my_magic": "slow.to.import",
316 "my_other_magic": "also.slow",
317 }
318
319 On first invocation of `%my_magic`, `%%my_magic`, `%%my_other_magic` or
320 `%%my_other_magic`, the corresponding module will be loaded as an ipython
321 extensions as if you had previously done `%load_ext ipython`.
322
323 Magics names should be without percent(s) as magics can be both cell
324 and line magics.
325
326 Lazy loading happen relatively late in execution process, and
327 complex extensions that manipulate Python/IPython internal state or global state
328 might not support lazy loading.
329 """
330 ).tag(
331 config=True,
332 )
305 333
306 334 # A registry of the original objects that we've been given holding magics.
307 335 registry = Dict()
@@ -366,6 +394,24 b' class MagicsManager(Configurable):'
366 394 docs[m_type] = m_docs
367 395 return docs
368 396
397 def register_lazy(self, name: str, fully_qualified_name: str):
398 """
399 Lazily register a magic via an extension.
400
401
402 Parameters
403 ----------
404 name : str
405 Name of the magic you wish to register.
406 fully_qualified_name :
407 Fully qualified name of the module/submodule that should be loaded
408 as an extensions when the magic is first called.
409 It is assumed that loading this extensions will register the given
410 magic.
411 """
412
413 self.lazy_magics[name] = fully_qualified_name
414
369 415 def register(self, *magic_objects):
370 416 """Register one or more instances of Magics.
371 417
@@ -34,9 +34,11 b' from IPython.testing import tools as tt'
34 34 from IPython.utils.io import capture_output
35 35 from IPython.utils.process import find_cmd
36 36 from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory
37 from IPython.utils.syspathcontext import prepended_to_syspath
37 38
38 39 from .test_debugger import PdbTestInput
39 40
41 from tempfile import NamedTemporaryFile
40 42
41 43 @magic.magics_class
42 44 class DummyMagics(magic.Magics): pass
@@ -1325,13 +1327,53 b' def test_timeit_arguments():'
1325 1327 _ip.magic("timeit -n1 -r1 a=('#')")
1326 1328
1327 1329
1330 MINIMAL_LAZY_MAGIC = """
1331 from IPython.core.magic import (
1332 Magics,
1333 magics_class,
1334 line_magic,
1335 cell_magic,
1336 )
1337
1338
1339 @magics_class
1340 class LazyMagics(Magics):
1341 @line_magic
1342 def lazy_line(self, line):
1343 print("Lazy Line")
1344
1345 @cell_magic
1346 def lazy_cell(self, line, cell):
1347 print("Lazy Cell")
1348
1349
1350 def load_ipython_extension(ipython):
1351 ipython.register_magics(LazyMagics)
1352 """
1353
1354
1355 def test_lazy_magics():
1356 with pytest.raises(UsageError):
1357 ip.run_line_magic("lazy_line", "")
1358
1359 startdir = os.getcwd()
1360
1361 with TemporaryDirectory() as tmpdir:
1362 with prepended_to_syspath(tmpdir):
1363 ptempdir = Path(tmpdir)
1364 tf = ptempdir / "lazy_magic_module.py"
1365 tf.write_text(MINIMAL_LAZY_MAGIC)
1366 ip.magics_manager.register_lazy("lazy_line", Path(tf.name).name[:-3])
1367 with tt.AssertPrints("Lazy Line"):
1368 ip.run_line_magic("lazy_line", "")
1369
1370
1328 1371 TEST_MODULE = """
1329 1372 print('Loaded my_tmp')
1330 1373 if __name__ == "__main__":
1331 1374 print('I just ran a script')
1332 1375 """
1333 1376
1334
1335 1377 def test_run_module_from_import_hook():
1336 1378 "Test that a module can be loaded via an import hook"
1337 1379 with TemporaryDirectory() as tmpdir:
@@ -9,11 +9,35 b' from io import StringIO'
9 9 from unittest import TestCase
10 10
11 11 from IPython.testing import tools as tt
12
13 12 #-----------------------------------------------------------------------------
14 13 # Test functions begin
15 14 #-----------------------------------------------------------------------------
16 15
16
17 MINIMAL_LAZY_MAGIC = """
18 from IPython.core.magic import (
19 Magics,
20 magics_class,
21 line_magic,
22 cell_magic,
23 )
24
25
26 @magics_class
27 class LazyMagics(Magics):
28 @line_magic
29 def lazy_line(self, line):
30 print("Lazy Line")
31
32 @cell_magic
33 def lazy_cell(self, line, cell):
34 print("Lazy Cell")
35
36
37 def load_ipython_extension(ipython):
38 ipython.register_magics(LazyMagics)
39 """
40
17 41 def check_cpaste(code, should_fail=False):
18 42 """Execute code via 'cpaste' and ensure it was executed, unless
19 43 should_fail is set.
@@ -31,7 +55,7 b' def check_cpaste(code, should_fail=False):'
31 55 try:
32 56 context = tt.AssertPrints if should_fail else tt.AssertNotPrints
33 57 with context("Traceback (most recent call last)"):
34 ip.magic('cpaste')
58 ip.run_line_magic("cpaste", "")
35 59
36 60 if not should_fail:
37 61 assert ip.user_ns['code_ran'], "%r failed" % code
@@ -68,13 +92,14 b' def test_cpaste():'
68 92 check_cpaste(code, should_fail=True)
69 93
70 94
95
71 96 class PasteTestCase(TestCase):
72 97 """Multiple tests for clipboard pasting"""
73 98
74 99 def paste(self, txt, flags='-q'):
75 100 """Paste input text, by default in quiet mode"""
76 101 ip.hooks.clipboard_get = lambda : txt
77 ip.magic('paste '+flags)
102 ip.run_line_magic("paste", flags)
78 103
79 104 def setUp(self):
80 105 # Inject fake clipboard hook but save original so we can restore it later
@@ -114,7 +139,7 b' class PasteTestCase(TestCase):'
114 139 self.assertEqual(ip.user_ns.pop("x"), [1, 2, 3])
115 140 self.assertEqual(ip.user_ns.pop("y"), [1, 4, 9])
116 141 self.assertFalse("x" in ip.user_ns)
117 ip.magic("paste -r")
142 ip.run_line_magic("paste", "-r")
118 143 self.assertEqual(ip.user_ns["x"], [1, 2, 3])
119 144 self.assertEqual(ip.user_ns["y"], [1, 4, 9])
120 145
@@ -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,
@@ -3,6 +3,24 b''
3 3 ============
4 4
5 5
6 .. _version 7.32:
7
8 IPython 7.32
9 ============
10
11
12 The ability to configure magics to be lazily loaded has been added to IPython.
13 See the ``ipython --help-all`` section on ``MagicsManager.lazy_magic``.
14 One can now use::
15
16 c.MagicsManger.lazy_magics = {
17 "my_magic": "slow.to.import",
18 "my_other_magic": "also.slow",
19 }
20
21 And on first use of ``%my_magic``, or corresponding cell magic, or other line magic,
22 the corresponding ``load_ext`` will be called just before trying to invoke the magic.
23
6 24 .. _version 7.31:
7 25
8 26 IPython 7.31
General Comments 0
You need to be logged in to leave comments. Login now