Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e5bbea0

Browse files
committed
Allow to configure lazy loadable magics.
While we encourage load_ipython_ext to be lazy and not take too much resources, it might not be practical especially when the top level module takes a long time to import. Here we allow to define a mapping between magics names and extension name, and on attempt to execute a non-existing magics we'll look into the lazy mapping and try to load it. We also add a helper to let us interactively register a magic lazily, see the `register_lazy()` method of to magics_manager
1 parent 17aedac commit e5bbea0

6 files changed

Lines changed: 175 additions & 11 deletions

File tree

IPython/core/interactiveshell.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,6 @@ def profile(self):
500500
def __init__(self, ipython_dir=None, profile_dir=None,
501501
user_module=None, user_ns=None,
502502
custom_exceptions=((), None), **kwargs):
503-
504503
# This is where traits with a config_key argument are updated
505504
# from the values on config.
506505
super(InteractiveShell, self).__init__(**kwargs)
@@ -1643,7 +1642,7 @@ def _inspect(self, meth, oname, namespaces=None, **kw):
16431642
formatter,
16441643
info,
16451644
enable_html_pager=self.enable_html_pager,
1646-
**kw
1645+
**kw,
16471646
)
16481647
else:
16491648
pmethod(info.obj, oname)
@@ -2173,7 +2172,34 @@ def register_magic_function(self, func, magic_kind='line', magic_name=None):
21732172
func, magic_kind=magic_kind, magic_name=magic_name
21742173
)
21752174

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):
21772203
"""Execute the given line magic.
21782204
21792205
Parameters
@@ -2186,7 +2212,12 @@ def run_line_magic(self, magic_name, line, _stack_depth=1):
21862212
If run_line_magic() is called from magic() then _stack_depth=2.
21872213
This is added to ensure backward compatibility for use of 'get_ipython().magic()'
21882214
"""
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)
21902221
if fn is None:
21912222
cm = self.find_cell_magic(magic_name)
21922223
etpl = "Line magic function `%%%s` not found%s."
@@ -2237,7 +2268,7 @@ def run_cell_magic(self, magic_name, line, cell):
22372268
cell : str
22382269
The body of the cell as a (possibly multiline) string.
22392270
"""
2240-
fn = self.find_cell_magic(magic_name)
2271+
fn = self._find_with_lazy_load("cell", magic_name)
22412272
if fn is None:
22422273
lm = self.find_line_magic(magic_name)
22432274
etpl = "Cell magic `%%{0}` not found{1}."

IPython/core/magic.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,34 @@ class MagicsManager(Configurable):
302302
# holding the actual callable object as value. This is the dict used for
303303
# magic function dispatch
304304
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+
)
305333

306334
# A registry of the original objects that we've been given holding magics.
307335
registry = Dict()
@@ -366,6 +394,24 @@ def lsmagic_docs(self, brief=False, missing=''):
366394
docs[m_type] = m_docs
367395
return docs
368396

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+
369415
def register(self, *magic_objects):
370416
"""Register one or more instances of Magics.
371417

IPython/core/tests/test_magic.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@
3434
from IPython.utils.io import capture_output
3535
from IPython.utils.process import find_cmd
3636
from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory
37+
from IPython.utils.syspathcontext import prepended_to_syspath
3738

3839
from .test_debugger import PdbTestInput
3940

41+
from tempfile import NamedTemporaryFile
4042

4143
@magic.magics_class
4244
class DummyMagics(magic.Magics): pass
@@ -1325,13 +1327,53 @@ def test_timeit_arguments():
13251327
_ip.magic("timeit -n1 -r1 a=('#')")
13261328

13271329

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+
13281371
TEST_MODULE = """
13291372
print('Loaded my_tmp')
13301373
if __name__ == "__main__":
13311374
print('I just ran a script')
13321375
"""
13331376

1334-
13351377
def test_run_module_from_import_hook():
13361378
"Test that a module can be loaded via an import hook"
13371379
with TemporaryDirectory() as tmpdir:

IPython/core/tests/test_magic_terminal.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,35 @@
99
from unittest import TestCase
1010

1111
from IPython.testing import tools as tt
12-
1312
#-----------------------------------------------------------------------------
1413
# Test functions begin
1514
#-----------------------------------------------------------------------------
1615

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+
1741
def check_cpaste(code, should_fail=False):
1842
"""Execute code via 'cpaste' and ensure it was executed, unless
1943
should_fail is set.
@@ -31,7 +55,7 @@ def check_cpaste(code, should_fail=False):
3155
try:
3256
context = tt.AssertPrints if should_fail else tt.AssertNotPrints
3357
with context("Traceback (most recent call last)"):
34-
ip.magic('cpaste')
58+
ip.run_line_magic("cpaste", "")
3559

3660
if not should_fail:
3761
assert ip.user_ns['code_ran'], "%r failed" % code
@@ -68,13 +92,14 @@ def runf():
6892
check_cpaste(code, should_fail=True)
6993

7094

95+
7196
class PasteTestCase(TestCase):
7297
"""Multiple tests for clipboard pasting"""
7398

7499
def paste(self, txt, flags='-q'):
75100
"""Paste input text, by default in quiet mode"""
76-
ip.hooks.clipboard_get = lambda : txt
77-
ip.magic('paste '+flags)
101+
ip.hooks.clipboard_get = lambda: txt
102+
ip.run_line_magic("paste", flags)
78103

79104
def setUp(self):
80105
# Inject fake clipboard hook but save original so we can restore it later
@@ -114,7 +139,7 @@ def test_paste_py_multi_r(self):
114139
self.assertEqual(ip.user_ns.pop("x"), [1, 2, 3])
115140
self.assertEqual(ip.user_ns.pop("y"), [1, 4, 9])
116141
self.assertFalse("x" in ip.user_ns)
117-
ip.magic("paste -r")
142+
ip.run_line_magic("paste", "-r")
118143
self.assertEqual(ip.user_ns["x"], [1, 2, 3])
119144
self.assertEqual(ip.user_ns["y"], [1, 4, 9])
120145

IPython/terminal/ipapp.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from IPython.core.application import (
2626
ProfileDir, BaseIPythonApplication, base_flags, base_aliases
2727
)
28+
from IPython.core.magic import MagicsManager
2829
from IPython.core.magics import (
2930
ScriptMagics, LoggingMagics
3031
)
@@ -200,6 +201,7 @@ def _classes_default(self):
200201
self.__class__, # it will also affect subclasses (e.g. QtConsole)
201202
TerminalInteractiveShell,
202203
HistoryManager,
204+
MagicsManager,
203205
ProfileDir,
204206
PlainTextFormatter,
205207
IPCompleter,

docs/source/whatsnew/version7.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@
33
============
44

55

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+
624
.. _version 7.31:
725

826
IPython 7.31

0 commit comments

Comments
 (0)