From d236bec178b19a9042512b800e6b54fb443dd5aa 2014-10-16 17:37:57 From: Min RK 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 `. :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