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. |
|
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. |
|
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 |
|
|
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 |
|
101 | ip.hooks.clipboard_get = lambda: txt | |
77 |
ip.magic( |
|
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