From 3e8f63d06e7d4f95bb84176d140d5eb3591d78c5 2014-09-08 23:09:29 From: Thomas Kluyver Date: 2014-09-08 23:09:29 Subject: [PATCH] Four possible states for completion reply, & indent hint --- diff --git a/IPython/core/inputsplitter.py b/IPython/core/inputsplitter.py index 37c0f42..c3ab255 100644 --- a/IPython/core/inputsplitter.py +++ b/IPython/core/inputsplitter.py @@ -224,6 +224,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. @@ -239,6 +241,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): @@ -248,19 +251,39 @@ class InputSplitter(object): self.reset() return out - def is_complete(self, source): + 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) - return not self.push_accepts_more() except SyntaxError: # Transformers in IPythonInputSplitter can raise SyntaxError, # which push() will not catch. - return True + 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() @@ -293,6 +316,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'): @@ -309,6 +333,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 f1f5af2..5cbcbe7 100644 --- a/IPython/core/tests/test_inputsplitter.py +++ b/IPython/core/tests/test_inputsplitter.py @@ -355,12 +355,12 @@ class InputSplitterTestCase(unittest.TestCase): isp.push(r"(1 \ ") self.assertFalse(isp.push_accepts_more()) - def test_is_complete(self): + def test_check_complete(self): isp = self.isp - assert isp.is_complete("a = 1") - assert not isp.is_complete("for a in range(5):") - assert isp.is_complete("raise = 2") # SyntaxError should mean complete - assert not isp.is_complete("a = [1,\n2,") + 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/tests/test_kernel.py b/IPython/kernel/tests/test_kernel.py index d4e4e88..1f9855f 100644 --- a/IPython/kernel/tests/test_kernel.py +++ b/IPython/kernel/tests/test_kernel.py @@ -211,13 +211,14 @@ def test_is_complete(): # that the kernel exposes the interface correctly. kc.is_complete('2+2') reply = kc.get_shell_msg(block=True, timeout=TIMEOUT) - assert reply['content']['complete'] + 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']['complete'] + assert reply['content']['status'] == 'invalid' kc.is_complete('a = [1,\n2,') reply = kc.get_shell_msg(block=True, timeout=TIMEOUT) - assert not reply['content']['complete'] + 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 57e4743..8260841 100644 --- a/IPython/kernel/tests/test_message_spec.py +++ b/IPython/kernel/tests/test_message_spec.py @@ -161,7 +161,15 @@ class KernelInfoReply(Reference): class IsCompleteReply(Reference): - complete = Bool() + 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 diff --git a/IPython/kernel/zmq/ipkernel.py b/IPython/kernel/zmq/ipkernel.py index 85512fd..0bcade6 100644 --- a/IPython/kernel/zmq/ipkernel.py +++ b/IPython/kernel/zmq/ipkernel.py @@ -230,8 +230,11 @@ class IPythonKernel(KernelBase): return dict(status='ok', restart=restart) def do_is_complete(self, code): - complete = self.shell.input_transformer_manager.is_complete(code) - return {'complete': complete} + 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 diff --git a/IPython/kernel/zmq/kernelbase.py b/IPython/kernel/zmq/kernelbase.py index 9b8511b..40e6c49 100755 --- a/IPython/kernel/zmq/kernelbase.py +++ b/IPython/kernel/zmq/kernelbase.py @@ -493,7 +493,7 @@ class Kernel(Configurable): def do_is_complete(self, code): """Override in subclasses to find completions. """ - return {'complete' : True, + return {'status' : 'unknown', } #--------------------------------------------------------------------------- diff --git a/docs/source/development/messaging.rst b/docs/source/development/messaging.rst index 4d6e1b9..67faa6d 100644 --- a/docs/source/development/messaging.rst +++ b/docs/source/development/messaging.rst @@ -585,9 +585,19 @@ 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. If the kernel does not reply promptly, -the frontend will probably default to sending the code to be executed. +execution or forcing a continuation prompt. Message type: ``is_complete_request``:: @@ -596,11 +606,17 @@ Message type: ``is_complete_request``:: 'code' : str, } -Message type: ``complete_reply``:: +Message type: ``is_complete_reply``:: content = { - # True if the code is ready to execute, False if not - 'complete' : bool, + # 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