From 1a856e1b905b271c497e8e292889230b4edfaba3 2012-05-26 03:28:24 From: Fernando Perez Date: 2012-05-26 03:28:24 Subject: [PATCH] First implementation of cell magics that goes via inputsplitter. The code is still ugly and probably somewhat fragile, but the basic idea is there. I need to clean things up and test in the qt console and terminal, where things aren't probably quite right yet. --- diff --git a/IPython/core/inputsplitter.py b/IPython/core/inputsplitter.py index 146104e..67403ce 100644 --- a/IPython/core/inputsplitter.py +++ b/IPython/core/inputsplitter.py @@ -55,7 +55,7 @@ Authors * Brian Granger """ #----------------------------------------------------------------------------- -# Copyright (C) 2010-2011 The IPython Development Team +# Copyright (C) 2010 The IPython Development Team # # Distributed under the terms of the BSD License. The full license is in # the file COPYING, distributed as part of this software. @@ -685,20 +685,23 @@ class IPythonInputSplitter(InputSplitter): # String with raw, untransformed input. source_raw = '' + cell_magic_body = None + # Private attributes # List with lines of raw input accumulated so far. _buffer_raw = None def __init__(self, input_mode=None): - InputSplitter.__init__(self, input_mode) + super(IPythonInputSplitter, self).__init__(input_mode) self._buffer_raw = [] def reset(self): """Reset the input buffer and associated state.""" - InputSplitter.reset(self) + super(IPythonInputSplitter, self).reset() self._buffer_raw[:] = [] self.source_raw = '' + self.cell_magic_body = None def source_raw_reset(self): """Return input and raw source and perform a full reset. @@ -710,6 +713,26 @@ class IPythonInputSplitter(InputSplitter): def push(self, lines): """Push one or more lines of IPython input. + + This stores the given lines and returns a status code indicating + whether the code forms a complete Python block or not, after processing + all input lines for special IPython syntax. + + Any exceptions generated in compilation are swallowed, but if an + exception was produced, the method returns True. + + Parameters + ---------- + lines : string + One or more lines of Python input. + + Returns + ------- + is_complete : boolean + True if the current input source (the result of the current input + plus prior inputs) forms a complete Python execution block. Note that + this value is also stored as a private attribute (_is_complete), so it + can be queried at any time. """ if not lines: return super(IPythonInputSplitter, self).push(lines) @@ -717,6 +740,20 @@ class IPythonInputSplitter(InputSplitter): # We must ensure all input is pure unicode lines = cast_unicode(lines, self.encoding) + # cell magic support + #print('IM:', self.input_mode,'\n'+lines); print('---') # dbg + #if self.input_mode == 'cell' and lines.startswith('%%'): + if lines.startswith('%%'): + # Cell magics bypass all further transformations + self.reset() + self._is_complete = is_complete = True + first, _, body = lines.partition('\n') + magic_name, _, line = first.partition(' ') + magic_name = magic_name.lstrip(ESC_MAGIC) + self.cell_magic_body = body + tpl = 'get_ipython()._cell_magic(%r, %r)' + lines = tpl % (magic_name, line) + lines_list = lines.splitlines() transforms = [transform_ipy_prompt, transform_classic_prompt, diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index e1c5355..0f96b3a 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2027,7 +2027,7 @@ class InteractiveShell(SingletonConfigurable): """ fn = self.find_line_magic(magic_name) if fn is None: - error("Magic function `%s` not found." % magic_name) + error("Line magic function `%%%s` not found." % magic_name) else: # Note: this is the distance in the stack to the user's frame. # This will need to be updated if the internal calling logic gets @@ -2048,7 +2048,7 @@ class InteractiveShell(SingletonConfigurable): """ fn = self.find_cell_magic(magic_name) if fn is None: - error("Magic function `%s` not found." % magic_name) + error("Cell magic function `%%%%%s` not found." % magic_name) else: # Note: this is the distance in the stack to the user's frame. # This will need to be updated if the internal calling logic gets @@ -2475,6 +2475,11 @@ class InteractiveShell(SingletonConfigurable): magic_name = magic_name.lstrip(prefilter.ESC_MAGIC) return self.cell_magic(magic_name, line, cell) + def _cell_magic(self, magic_name, line): + cell = self._current_cell_magic_body + self._current_cell_magic_body = None + return self.cell_magic(magic_name, line, cell) + def run_cell(self, raw_cell, store_history=False, silent=False): """Run a complete IPython cell. @@ -2496,11 +2501,14 @@ class InteractiveShell(SingletonConfigurable): if silent: store_history = False - if raw_cell.startswith('%%'): - return self.call_cell_magic(raw_cell, store_history) + self.input_splitter.push(raw_cell) - for line in raw_cell.splitlines(): - self.input_splitter.push(line) + # Check for cell magics, which leave state behind. This interface is + # ugly, we need to do something cleaner later... Now the logic is + # simply that the input_splitter remembers if there was a cell magic, + # and in that case we grab the cell body. + if self.input_splitter.cell_magic_body is not None: + self._current_cell_magic_body = self.input_splitter.cell_magic_body cell = self.input_splitter.source_reset() with self.builtin_trap: diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 5d51638..362eb89 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -495,10 +495,12 @@ def test_env(): class CellMagicTestCase(TestCase): def check_ident(self, magic): + # Manually called, we get the result out = _ip.cell_magic(magic, 'a', 'b') nt.assert_equals(out, ('a','b')) - out = _ip.run_cell('%%' + magic +' a\nb') - nt.assert_equals(out, ('a','b')) + # Via run_cell, it goes into the user's namespace via displayhook + _ip.run_cell('%%' + magic +' c\nd') + nt.assert_equals(_ip.user_ns['_'], ('c','d')) def test_cell_magic_func_deco(self): "Cell magic using simple decorator" @@ -525,12 +527,19 @@ class CellMagicTestCase(TestCase): def cellm3(self, line, cell): return line, cell + _ip.register_magics(MyMagics) + self.check_ident('cellm3') + + def test_cell_magic_class2(self): + "Cell magics declared via a class, #2" + @magics_class + class MyMagics2(Magics): + @cell_magic('cellm4') def cellm33(self, line, cell): return line, cell - - _ip.register_magics(MyMagics) - self.check_ident('cellm3') + + _ip.register_magics(MyMagics2) self.check_ident('cellm4') # Check that nothing is registered as 'cellm33' c33 = _ip.find_cell_magic('cellm33')