##// END OF EJS Templates
Merge pull request #13506 from Carreau/lazy-magics...
Matthias Bussonnier -
r27505:ca8b6d57 merge
parent child Browse files
Show More
@@ -500,7 +500,6 b' class InteractiveShell(SingletonConfigurable):'
500 def __init__(self, ipython_dir=None, profile_dir=None,
500 def __init__(self, ipython_dir=None, profile_dir=None,
501 user_module=None, user_ns=None,
501 user_module=None, user_ns=None,
502 custom_exceptions=((), None), **kwargs):
502 custom_exceptions=((), None), **kwargs):
503
504 # This is where traits with a config_key argument are updated
503 # This is where traits with a config_key argument are updated
505 # from the values on config.
504 # from the values on config.
506 super(InteractiveShell, self).__init__(**kwargs)
505 super(InteractiveShell, self).__init__(**kwargs)
@@ -1643,7 +1642,7 b' class InteractiveShell(SingletonConfigurable):'
1643 formatter,
1642 formatter,
1644 info,
1643 info,
1645 enable_html_pager=self.enable_html_pager,
1644 enable_html_pager=self.enable_html_pager,
1646 **kw
1645 **kw,
1647 )
1646 )
1648 else:
1647 else:
1649 pmethod(info.obj, oname)
1648 pmethod(info.obj, oname)
@@ -2173,7 +2172,34 b' class InteractiveShell(SingletonConfigurable):'
2173 func, magic_kind=magic_kind, magic_name=magic_name
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 """Execute the given line magic.
2203 """Execute the given line magic.
2178
2204
2179 Parameters
2205 Parameters
@@ -2186,7 +2212,12 b' class InteractiveShell(SingletonConfigurable):'
2186 If run_line_magic() is called from magic() then _stack_depth=2.
2212 If run_line_magic() is called from magic() then _stack_depth=2.
2187 This is added to ensure backward compatibility for use of 'get_ipython().magic()'
2213 This is added to ensure backward compatibility for use of 'get_ipython().magic()'
2188 """
2214 """
2189 fn = self.find_line_magic(magic_name)
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)
2220 fn = self.find_line_magic(magic_name)
2190 if fn is None:
2221 if fn is None:
2191 cm = self.find_cell_magic(magic_name)
2222 cm = self.find_cell_magic(magic_name)
2192 etpl = "Line magic function `%%%s` not found%s."
2223 etpl = "Line magic function `%%%s` not found%s."
@@ -2237,7 +2268,7 b' class InteractiveShell(SingletonConfigurable):'
2237 cell : str
2268 cell : str
2238 The body of the cell as a (possibly multiline) string.
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 if fn is None:
2272 if fn is None:
2242 lm = self.find_line_magic(magic_name)
2273 lm = self.find_line_magic(magic_name)
2243 etpl = "Cell magic `%%{0}` not found{1}."
2274 etpl = "Cell magic `%%{0}` not found{1}."
@@ -302,6 +302,34 b' class MagicsManager(Configurable):'
302 # holding the actual callable object as value. This is the dict used for
302 # holding the actual callable object as value. This is the dict used for
303 # magic function dispatch
303 # magic function dispatch
304 magics = Dict()
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 # A registry of the original objects that we've been given holding magics.
334 # A registry of the original objects that we've been given holding magics.
307 registry = Dict()
335 registry = Dict()
@@ -366,6 +394,24 b' class MagicsManager(Configurable):'
366 docs[m_type] = m_docs
394 docs[m_type] = m_docs
367 return docs
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 def register(self, *magic_objects):
415 def register(self, *magic_objects):
370 """Register one or more instances of Magics.
416 """Register one or more instances of Magics.
371
417
@@ -34,9 +34,11 b' from IPython.testing import tools as tt'
34 from IPython.utils.io import capture_output
34 from IPython.utils.io import capture_output
35 from IPython.utils.process import find_cmd
35 from IPython.utils.process import find_cmd
36 from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory
36 from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory
37 from IPython.utils.syspathcontext import prepended_to_syspath
37
38
38 from .test_debugger import PdbTestInput
39 from .test_debugger import PdbTestInput
39
40
41 from tempfile import NamedTemporaryFile
40
42
41 @magic.magics_class
43 @magic.magics_class
42 class DummyMagics(magic.Magics): pass
44 class DummyMagics(magic.Magics): pass
@@ -1325,13 +1327,53 b' def test_timeit_arguments():'
1325 _ip.magic("timeit -n1 -r1 a=('#')")
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 TEST_MODULE = """
1371 TEST_MODULE = """
1329 print('Loaded my_tmp')
1372 print('Loaded my_tmp')
1330 if __name__ == "__main__":
1373 if __name__ == "__main__":
1331 print('I just ran a script')
1374 print('I just ran a script')
1332 """
1375 """
1333
1376
1334
1335 def test_run_module_from_import_hook():
1377 def test_run_module_from_import_hook():
1336 "Test that a module can be loaded via an import hook"
1378 "Test that a module can be loaded via an import hook"
1337 with TemporaryDirectory() as tmpdir:
1379 with TemporaryDirectory() as tmpdir:
@@ -9,11 +9,35 b' from io import StringIO'
9 from unittest import TestCase
9 from unittest import TestCase
10
10
11 from IPython.testing import tools as tt
11 from IPython.testing import tools as tt
12
13 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
14 # Test functions begin
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 def check_cpaste(code, should_fail=False):
41 def check_cpaste(code, should_fail=False):
18 """Execute code via 'cpaste' and ensure it was executed, unless
42 """Execute code via 'cpaste' and ensure it was executed, unless
19 should_fail is set.
43 should_fail is set.
@@ -31,7 +55,7 b' def check_cpaste(code, should_fail=False):'
31 try:
55 try:
32 context = tt.AssertPrints if should_fail else tt.AssertNotPrints
56 context = tt.AssertPrints if should_fail else tt.AssertNotPrints
33 with context("Traceback (most recent call last)"):
57 with context("Traceback (most recent call last)"):
34 ip.magic('cpaste')
58 ip.run_line_magic("cpaste", "")
35
59
36 if not should_fail:
60 if not should_fail:
37 assert ip.user_ns['code_ran'], "%r failed" % code
61 assert ip.user_ns['code_ran'], "%r failed" % code
@@ -68,13 +92,14 b' def test_cpaste():'
68 check_cpaste(code, should_fail=True)
92 check_cpaste(code, should_fail=True)
69
93
70
94
95
71 class PasteTestCase(TestCase):
96 class PasteTestCase(TestCase):
72 """Multiple tests for clipboard pasting"""
97 """Multiple tests for clipboard pasting"""
73
98
74 def paste(self, txt, flags='-q'):
99 def paste(self, txt, flags='-q'):
75 """Paste input text, by default in quiet mode"""
100 """Paste input text, by default in quiet mode"""
76 ip.hooks.clipboard_get = lambda : txt
101 ip.hooks.clipboard_get = lambda: txt
77 ip.magic('paste '+flags)
102 ip.run_line_magic("paste", flags)
78
103
79 def setUp(self):
104 def setUp(self):
80 # Inject fake clipboard hook but save original so we can restore it later
105 # Inject fake clipboard hook but save original so we can restore it later
@@ -114,7 +139,7 b' class PasteTestCase(TestCase):'
114 self.assertEqual(ip.user_ns.pop("x"), [1, 2, 3])
139 self.assertEqual(ip.user_ns.pop("x"), [1, 2, 3])
115 self.assertEqual(ip.user_ns.pop("y"), [1, 4, 9])
140 self.assertEqual(ip.user_ns.pop("y"), [1, 4, 9])
116 self.assertFalse("x" in ip.user_ns)
141 self.assertFalse("x" in ip.user_ns)
117 ip.magic("paste -r")
142 ip.run_line_magic("paste", "-r")
118 self.assertEqual(ip.user_ns["x"], [1, 2, 3])
143 self.assertEqual(ip.user_ns["x"], [1, 2, 3])
119 self.assertEqual(ip.user_ns["y"], [1, 4, 9])
144 self.assertEqual(ip.user_ns["y"], [1, 4, 9])
120
145
@@ -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,
@@ -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 .. _version 7.31:
24 .. _version 7.31:
7
25
8 IPython 7.31
26 IPython 7.31
General Comments 0
You need to be logged in to leave comments. Login now