From d236bec178b19a9042512b800e6b54fb443dd5aa 2014-10-16 17:37:57
From: Min RK <benjaminrk@gmail.com>
Date: 2014-10-16 17:37:57
Subject: [PATCH] Merge pull request #6297 from takluyver/is-complete-request

is_complete messages
---

diff --git a/IPython/core/inputsplitter.py b/IPython/core/inputsplitter.py
index 5d1a90d..5c48bca 100644
--- a/IPython/core/inputsplitter.py
+++ b/IPython/core/inputsplitter.py
@@ -208,6 +208,8 @@ class InputSplitter(object):
     _full_dedent = False
     # Boolean indicating whether the current block is complete
     _is_complete = None
+    # Boolean indicating whether the current block has an unrecoverable syntax error
+    _is_invalid = False
 
     def __init__(self):
         """Create a new InputSplitter instance.
@@ -223,6 +225,7 @@ class InputSplitter(object):
         self.source = ''
         self.code = None
         self._is_complete = False
+        self._is_invalid = False
         self._full_dedent = False
 
     def source_reset(self):
@@ -232,6 +235,42 @@ class InputSplitter(object):
         self.reset()
         return out
 
+    def check_complete(self, source):
+        """Return whether a block of code is ready to execute, or should be continued
+        
+        This is a non-stateful API, and will reset the state of this InputSplitter.
+        
+        Parameters
+        ----------
+        source : string
+          Python input code, which can be multiline.
+        
+        Returns
+        -------
+        status : str
+          One of 'complete', 'incomplete', or 'invalid' if source is not a
+          prefix of valid code.
+        indent_spaces : int or None
+          The number of spaces by which to indent the next line of code. If
+          status is not 'incomplete', this is None.
+        """
+        self.reset()
+        try:
+            self.push(source)
+        except SyntaxError:
+            # Transformers in IPythonInputSplitter can raise SyntaxError,
+            # which push() will not catch.
+            return 'invalid', None
+        else:
+            if self._is_invalid:
+                return 'invalid', None
+            elif self.push_accepts_more():
+                return 'incomplete', self.indent_spaces
+            else:
+                return 'complete', None
+        finally:
+            self.reset()
+
     def push(self, lines):
         """Push one or more lines of input.
 
@@ -261,6 +300,7 @@ class InputSplitter(object):
         # exception is raised in compilation, we don't mislead by having
         # inconsistent code/source attributes.
         self.code, self._is_complete = None, None
+        self._is_invalid = False
 
         # Honor termination lines properly
         if source.endswith('\\\n'):
@@ -277,6 +317,7 @@ class InputSplitter(object):
         except (SyntaxError, OverflowError, ValueError, TypeError,
                 MemoryError):
             self._is_complete = True
+            self._is_invalid = True
         else:
             # Compilation didn't produce any exceptions (though it may not have
             # given a complete code object)
diff --git a/IPython/core/tests/test_inputsplitter.py b/IPython/core/tests/test_inputsplitter.py
index 6447f0c..e2ae120 100644
--- a/IPython/core/tests/test_inputsplitter.py
+++ b/IPython/core/tests/test_inputsplitter.py
@@ -342,6 +342,13 @@ class InputSplitterTestCase(unittest.TestCase):
         isp.push(r"(1 \ ")
         self.assertFalse(isp.push_accepts_more())
 
+    def test_check_complete(self):
+        isp = self.isp
+        self.assertEqual(isp.check_complete("a = 1"), ('complete', None))
+        self.assertEqual(isp.check_complete("for a in range(5):"), ('incomplete', 4))
+        self.assertEqual(isp.check_complete("raise = 2"), ('invalid', None))
+        self.assertEqual(isp.check_complete("a = [1,\n2,"), ('incomplete', 0))
+
 class InteractiveLoopTestCase(unittest.TestCase):
     """Tests for an interactive loop like a python shell.
     """
diff --git a/IPython/kernel/channels.py b/IPython/kernel/channels.py
index 24c8b3f..c228bab 100644
--- a/IPython/kernel/channels.py
+++ b/IPython/kernel/channels.py
@@ -194,6 +194,7 @@ class ShellChannel(ZMQSocketChannel):
         'history',
         'kernel_info',
         'shutdown',
+        'is_complete',
     ]
 
     def __init__(self, context, session, address):
@@ -396,6 +397,10 @@ class ShellChannel(ZMQSocketChannel):
         self._queue_send(msg)
         return msg['header']['msg_id']
 
+    def is_complete(self, code):
+        msg = self.session.msg('is_complete_request', {'code': code})
+        self._queue_send(msg)
+        return msg['header']['msg_id']
 
 
 class IOPubChannel(ZMQSocketChannel):
diff --git a/IPython/kernel/tests/test_kernel.py b/IPython/kernel/tests/test_kernel.py
index 5c3272f..1f9855f 100644
--- a/IPython/kernel/tests/test_kernel.py
+++ b/IPython/kernel/tests/test_kernel.py
@@ -205,3 +205,20 @@ def test_help_output():
     """ipython kernel --help-all works"""
     tt.help_all_output_test('kernel')
 
+def test_is_complete():
+    with kernel() as kc:
+        # There are more test cases for this in core - here we just check
+        # that the kernel exposes the interface correctly.
+        kc.is_complete('2+2')
+        reply = kc.get_shell_msg(block=True, timeout=TIMEOUT)
+        assert reply['content']['status'] == 'complete'
+
+        # SyntaxError should mean it's complete        
+        kc.is_complete('raise = 2')
+        reply = kc.get_shell_msg(block=True, timeout=TIMEOUT)
+        assert reply['content']['status'] == 'invalid'
+        
+        kc.is_complete('a = [1,\n2,')
+        reply = kc.get_shell_msg(block=True, timeout=TIMEOUT)
+        assert reply['content']['status'] == 'incomplete'
+        assert reply['content']['indent'] == ''
\ No newline at end of file
diff --git a/IPython/kernel/tests/test_message_spec.py b/IPython/kernel/tests/test_message_spec.py
index 23d24c8..3716a9f 100644
--- a/IPython/kernel/tests/test_message_spec.py
+++ b/IPython/kernel/tests/test_message_spec.py
@@ -160,6 +160,18 @@ class KernelInfoReply(Reference):
     banner = Unicode()
 
 
+class IsCompleteReply(Reference):
+    status = Enum((u'complete', u'incomplete', u'invalid', u'unknown'))
+    
+    def check(self, d):
+        Reference.check(self, d)
+        if d['status'] == 'incomplete':
+            IsCompleteReplyIncomplete().check(d)
+
+class IsCompleteReplyIncomplete(Reference):
+    indent = Unicode()
+
+
 # IOPub messages
 
 class ExecuteInput(Reference):
@@ -189,6 +201,7 @@ references = {
     'status' : Status(),
     'complete_reply' : CompleteReply(),
     'kernel_info_reply': KernelInfoReply(),
+    'is_complete_reply': IsCompleteReply(),
     'execute_input' : ExecuteInput(),
     'execute_result' : ExecuteResult(),
     'error' : Error(),
@@ -382,6 +395,12 @@ def test_single_payload():
     next_input_pls = [pl for pl in payload if pl["source"] == "set_next_input"]
     nt.assert_equal(len(next_input_pls), 1)
 
+def test_is_complete():
+    flush_channels()
+    
+    msg_id = KC.is_complete("a = 1")
+    reply = KC.get_shell_msg(timeout=TIMEOUT)
+    validate_message(reply, 'is_complete_reply', msg_id)
 
 # IOPub channel
 
diff --git a/IPython/kernel/zmq/ipkernel.py b/IPython/kernel/zmq/ipkernel.py
index 7354e65..793cd48 100644
--- a/IPython/kernel/zmq/ipkernel.py
+++ b/IPython/kernel/zmq/ipkernel.py
@@ -232,6 +232,13 @@ class IPythonKernel(KernelBase):
         self.shell.exit_now = True
         return dict(status='ok', restart=restart)
 
+    def do_is_complete(self, code):
+        status, indent_spaces = self.shell.input_transformer_manager.check_complete(code)
+        r = {'status': status}
+        if status == 'incomplete':
+            r['indent'] = ' ' * indent_spaces
+        return r
+
     def do_apply(self, content, bufs, msg_id, reply_metadata):
         shell = self.shell
         try:
diff --git a/IPython/kernel/zmq/kernelbase.py b/IPython/kernel/zmq/kernelbase.py
index 30677ca..eb76981 100755
--- a/IPython/kernel/zmq/kernelbase.py
+++ b/IPython/kernel/zmq/kernelbase.py
@@ -114,7 +114,7 @@ class Kernel(SingletonConfigurable):
                       'inspect_request', 'history_request',
                       'kernel_info_request',
                       'connect_request', 'shutdown_request',
-                      'apply_request',
+                      'apply_request', 'is_complete_request',
                     ]
         self.shell_handlers = {}
         for msg_type in msg_types:
@@ -479,6 +479,22 @@ class Kernel(SingletonConfigurable):
         kernel.
         """
         return {'status': 'ok', 'restart': restart}
+    
+    def is_complete_request(self, stream, ident, parent):
+        content = parent['content']
+        code = content['code']
+        
+        reply_content = self.do_is_complete(code)
+        reply_content = json_clean(reply_content)
+        reply_msg = self.session.send(stream, 'is_complete_reply',
+                                           reply_content, parent, ident)
+        self.log.debug("%s", reply_msg)
+
+    def do_is_complete(self, code):
+        """Override in subclasses to find completions.
+        """
+        return {'status' : 'unknown',
+                }
 
     #---------------------------------------------------------------------------
     # Engine methods
diff --git a/docs/source/development/messaging.rst b/docs/source/development/messaging.rst
index 5f21886..f2a3be1 100644
--- a/docs/source/development/messaging.rst
+++ b/docs/source/development/messaging.rst
@@ -573,6 +573,51 @@ Message type: ``history_reply``::
       'history' : list,
     }
 
+.. _msging_is_complete:
+
+Code completeness
+-----------------
+
+.. versionadded:: 5.0
+
+When the user enters a line in a console style interface, the console must
+decide whether to immediately execute the current code, or whether to show a
+continuation prompt for further input. For instance, in Python ``a = 5`` would
+be executed immediately, while ``for i in range(5):`` would expect further input.
+
+There are four possible replies:
+
+- *complete* code is ready to be executed
+- *incomplete* code should prompt for another line
+- *invalid* code will typically be sent for execution, so that the user sees the
+  error soonest.
+- *unknown* - if the kernel is not able to determine this. The frontend should
+  also handle the kernel not replying promptly. It may default to sending the
+  code for execution, or it may implement simple fallback heuristics for whether
+  to execute the code (e.g. execute after a blank line).
+
+Frontends may have ways to override this, forcing the code to be sent for
+execution or forcing a continuation prompt.
+
+Message type: ``is_complete_request``::
+
+    content = {
+        # The code entered so far as a multiline string
+        'code' : str,
+    }
+
+Message type: ``is_complete_reply``::
+
+    content = {
+        # One of 'complete', 'incomplete', 'invalid', 'unknown'
+        'status' : str,
+        
+        # If status is 'incomplete', indent should contain the characters to use
+        # to indent the next line. This is only a hint: frontends may ignore it
+        # and use their own autoindentation rules. For other statuses, this
+        # field does not exist.
+        'indent': str,
+    }
 
 Connect
 -------
diff --git a/docs/source/development/wrapperkernels.rst b/docs/source/development/wrapperkernels.rst
index a8817a0..3e1db4e 100644
--- a/docs/source/development/wrapperkernels.rst
+++ b/docs/source/development/wrapperkernels.rst
@@ -141,6 +141,17 @@ relevant section of the :doc:`messaging spec <messaging>`.
      
         :ref:`msging_history` messages
 
+   .. method:: do_is_complete(code)
+   
+     Is code entered in a console-like interface complete and ready to execute,
+     or should a continuation prompt be shown?
+     
+     :param str code: The code entered so far - possibly multiple lines
+     
+     .. seealso::
+     
+        :ref:`msging_is_complete` messages
+
    .. method:: do_shutdown(restart)
 
      Shutdown the kernel. You only need to handle your own clean up - the kernel