Show More
@@ -208,6 +208,8 b' class InputSplitter(object):' | |||||
208 | _full_dedent = False |
|
208 | _full_dedent = False | |
209 | # Boolean indicating whether the current block is complete |
|
209 | # Boolean indicating whether the current block is complete | |
210 | _is_complete = None |
|
210 | _is_complete = None | |
|
211 | # Boolean indicating whether the current block has an unrecoverable syntax error | |||
|
212 | _is_invalid = False | |||
211 |
|
213 | |||
212 | def __init__(self): |
|
214 | def __init__(self): | |
213 | """Create a new InputSplitter instance. |
|
215 | """Create a new InputSplitter instance. | |
@@ -223,6 +225,7 b' class InputSplitter(object):' | |||||
223 | self.source = '' |
|
225 | self.source = '' | |
224 | self.code = None |
|
226 | self.code = None | |
225 | self._is_complete = False |
|
227 | self._is_complete = False | |
|
228 | self._is_invalid = False | |||
226 | self._full_dedent = False |
|
229 | self._full_dedent = False | |
227 |
|
230 | |||
228 | def source_reset(self): |
|
231 | def source_reset(self): | |
@@ -232,6 +235,42 b' class InputSplitter(object):' | |||||
232 | self.reset() |
|
235 | self.reset() | |
233 | return out |
|
236 | return out | |
234 |
|
237 | |||
|
238 | def check_complete(self, source): | |||
|
239 | """Return whether a block of code is ready to execute, or should be continued | |||
|
240 | ||||
|
241 | This is a non-stateful API, and will reset the state of this InputSplitter. | |||
|
242 | ||||
|
243 | Parameters | |||
|
244 | ---------- | |||
|
245 | source : string | |||
|
246 | Python input code, which can be multiline. | |||
|
247 | ||||
|
248 | Returns | |||
|
249 | ------- | |||
|
250 | status : str | |||
|
251 | One of 'complete', 'incomplete', or 'invalid' if source is not a | |||
|
252 | prefix of valid code. | |||
|
253 | indent_spaces : int or None | |||
|
254 | The number of spaces by which to indent the next line of code. If | |||
|
255 | status is not 'incomplete', this is None. | |||
|
256 | """ | |||
|
257 | self.reset() | |||
|
258 | try: | |||
|
259 | self.push(source) | |||
|
260 | except SyntaxError: | |||
|
261 | # Transformers in IPythonInputSplitter can raise SyntaxError, | |||
|
262 | # which push() will not catch. | |||
|
263 | return 'invalid', None | |||
|
264 | else: | |||
|
265 | if self._is_invalid: | |||
|
266 | return 'invalid', None | |||
|
267 | elif self.push_accepts_more(): | |||
|
268 | return 'incomplete', self.indent_spaces | |||
|
269 | else: | |||
|
270 | return 'complete', None | |||
|
271 | finally: | |||
|
272 | self.reset() | |||
|
273 | ||||
235 | def push(self, lines): |
|
274 | def push(self, lines): | |
236 | """Push one or more lines of input. |
|
275 | """Push one or more lines of input. | |
237 |
|
276 | |||
@@ -261,6 +300,7 b' class InputSplitter(object):' | |||||
261 | # exception is raised in compilation, we don't mislead by having |
|
300 | # exception is raised in compilation, we don't mislead by having | |
262 | # inconsistent code/source attributes. |
|
301 | # inconsistent code/source attributes. | |
263 | self.code, self._is_complete = None, None |
|
302 | self.code, self._is_complete = None, None | |
|
303 | self._is_invalid = False | |||
264 |
|
304 | |||
265 | # Honor termination lines properly |
|
305 | # Honor termination lines properly | |
266 | if source.endswith('\\\n'): |
|
306 | if source.endswith('\\\n'): | |
@@ -277,6 +317,7 b' class InputSplitter(object):' | |||||
277 | except (SyntaxError, OverflowError, ValueError, TypeError, |
|
317 | except (SyntaxError, OverflowError, ValueError, TypeError, | |
278 | MemoryError): |
|
318 | MemoryError): | |
279 | self._is_complete = True |
|
319 | self._is_complete = True | |
|
320 | self._is_invalid = True | |||
280 | else: |
|
321 | else: | |
281 | # Compilation didn't produce any exceptions (though it may not have |
|
322 | # Compilation didn't produce any exceptions (though it may not have | |
282 | # given a complete code object) |
|
323 | # given a complete code object) |
@@ -342,6 +342,13 b' class InputSplitterTestCase(unittest.TestCase):' | |||||
342 | isp.push(r"(1 \ ") |
|
342 | isp.push(r"(1 \ ") | |
343 | self.assertFalse(isp.push_accepts_more()) |
|
343 | self.assertFalse(isp.push_accepts_more()) | |
344 |
|
344 | |||
|
345 | def test_check_complete(self): | |||
|
346 | isp = self.isp | |||
|
347 | self.assertEqual(isp.check_complete("a = 1"), ('complete', None)) | |||
|
348 | self.assertEqual(isp.check_complete("for a in range(5):"), ('incomplete', 4)) | |||
|
349 | self.assertEqual(isp.check_complete("raise = 2"), ('invalid', None)) | |||
|
350 | self.assertEqual(isp.check_complete("a = [1,\n2,"), ('incomplete', 0)) | |||
|
351 | ||||
345 | class InteractiveLoopTestCase(unittest.TestCase): |
|
352 | class InteractiveLoopTestCase(unittest.TestCase): | |
346 | """Tests for an interactive loop like a python shell. |
|
353 | """Tests for an interactive loop like a python shell. | |
347 | """ |
|
354 | """ |
@@ -194,6 +194,7 b' class ShellChannel(ZMQSocketChannel):' | |||||
194 | 'history', |
|
194 | 'history', | |
195 | 'kernel_info', |
|
195 | 'kernel_info', | |
196 | 'shutdown', |
|
196 | 'shutdown', | |
|
197 | 'is_complete', | |||
197 | ] |
|
198 | ] | |
198 |
|
199 | |||
199 | def __init__(self, context, session, address): |
|
200 | def __init__(self, context, session, address): | |
@@ -396,6 +397,10 b' class ShellChannel(ZMQSocketChannel):' | |||||
396 | self._queue_send(msg) |
|
397 | self._queue_send(msg) | |
397 | return msg['header']['msg_id'] |
|
398 | return msg['header']['msg_id'] | |
398 |
|
399 | |||
|
400 | def is_complete(self, code): | |||
|
401 | msg = self.session.msg('is_complete_request', {'code': code}) | |||
|
402 | self._queue_send(msg) | |||
|
403 | return msg['header']['msg_id'] | |||
399 |
|
404 | |||
400 |
|
405 | |||
401 | class IOPubChannel(ZMQSocketChannel): |
|
406 | class IOPubChannel(ZMQSocketChannel): |
@@ -205,3 +205,20 b' def test_help_output():' | |||||
205 | """ipython kernel --help-all works""" |
|
205 | """ipython kernel --help-all works""" | |
206 | tt.help_all_output_test('kernel') |
|
206 | tt.help_all_output_test('kernel') | |
207 |
|
207 | |||
|
208 | def test_is_complete(): | |||
|
209 | with kernel() as kc: | |||
|
210 | # There are more test cases for this in core - here we just check | |||
|
211 | # that the kernel exposes the interface correctly. | |||
|
212 | kc.is_complete('2+2') | |||
|
213 | reply = kc.get_shell_msg(block=True, timeout=TIMEOUT) | |||
|
214 | assert reply['content']['status'] == 'complete' | |||
|
215 | ||||
|
216 | # SyntaxError should mean it's complete | |||
|
217 | kc.is_complete('raise = 2') | |||
|
218 | reply = kc.get_shell_msg(block=True, timeout=TIMEOUT) | |||
|
219 | assert reply['content']['status'] == 'invalid' | |||
|
220 | ||||
|
221 | kc.is_complete('a = [1,\n2,') | |||
|
222 | reply = kc.get_shell_msg(block=True, timeout=TIMEOUT) | |||
|
223 | assert reply['content']['status'] == 'incomplete' | |||
|
224 | assert reply['content']['indent'] == '' No newline at end of file |
@@ -160,6 +160,18 b' class KernelInfoReply(Reference):' | |||||
160 | banner = Unicode() |
|
160 | banner = Unicode() | |
161 |
|
161 | |||
162 |
|
162 | |||
|
163 | class IsCompleteReply(Reference): | |||
|
164 | status = Enum((u'complete', u'incomplete', u'invalid', u'unknown')) | |||
|
165 | ||||
|
166 | def check(self, d): | |||
|
167 | Reference.check(self, d) | |||
|
168 | if d['status'] == 'incomplete': | |||
|
169 | IsCompleteReplyIncomplete().check(d) | |||
|
170 | ||||
|
171 | class IsCompleteReplyIncomplete(Reference): | |||
|
172 | indent = Unicode() | |||
|
173 | ||||
|
174 | ||||
163 | # IOPub messages |
|
175 | # IOPub messages | |
164 |
|
176 | |||
165 | class ExecuteInput(Reference): |
|
177 | class ExecuteInput(Reference): | |
@@ -189,6 +201,7 b' references = {' | |||||
189 | 'status' : Status(), |
|
201 | 'status' : Status(), | |
190 | 'complete_reply' : CompleteReply(), |
|
202 | 'complete_reply' : CompleteReply(), | |
191 | 'kernel_info_reply': KernelInfoReply(), |
|
203 | 'kernel_info_reply': KernelInfoReply(), | |
|
204 | 'is_complete_reply': IsCompleteReply(), | |||
192 | 'execute_input' : ExecuteInput(), |
|
205 | 'execute_input' : ExecuteInput(), | |
193 | 'execute_result' : ExecuteResult(), |
|
206 | 'execute_result' : ExecuteResult(), | |
194 | 'error' : Error(), |
|
207 | 'error' : Error(), | |
@@ -382,6 +395,12 b' def test_single_payload():' | |||||
382 | next_input_pls = [pl for pl in payload if pl["source"] == "set_next_input"] |
|
395 | next_input_pls = [pl for pl in payload if pl["source"] == "set_next_input"] | |
383 | nt.assert_equal(len(next_input_pls), 1) |
|
396 | nt.assert_equal(len(next_input_pls), 1) | |
384 |
|
397 | |||
|
398 | def test_is_complete(): | |||
|
399 | flush_channels() | |||
|
400 | ||||
|
401 | msg_id = KC.is_complete("a = 1") | |||
|
402 | reply = KC.get_shell_msg(timeout=TIMEOUT) | |||
|
403 | validate_message(reply, 'is_complete_reply', msg_id) | |||
385 |
|
404 | |||
386 | # IOPub channel |
|
405 | # IOPub channel | |
387 |
|
406 |
@@ -232,6 +232,13 b' class IPythonKernel(KernelBase):' | |||||
232 | self.shell.exit_now = True |
|
232 | self.shell.exit_now = True | |
233 | return dict(status='ok', restart=restart) |
|
233 | return dict(status='ok', restart=restart) | |
234 |
|
234 | |||
|
235 | def do_is_complete(self, code): | |||
|
236 | status, indent_spaces = self.shell.input_transformer_manager.check_complete(code) | |||
|
237 | r = {'status': status} | |||
|
238 | if status == 'incomplete': | |||
|
239 | r['indent'] = ' ' * indent_spaces | |||
|
240 | return r | |||
|
241 | ||||
235 | def do_apply(self, content, bufs, msg_id, reply_metadata): |
|
242 | def do_apply(self, content, bufs, msg_id, reply_metadata): | |
236 | shell = self.shell |
|
243 | shell = self.shell | |
237 | try: |
|
244 | try: |
@@ -114,7 +114,7 b' class Kernel(SingletonConfigurable):' | |||||
114 | 'inspect_request', 'history_request', |
|
114 | 'inspect_request', 'history_request', | |
115 | 'kernel_info_request', |
|
115 | 'kernel_info_request', | |
116 | 'connect_request', 'shutdown_request', |
|
116 | 'connect_request', 'shutdown_request', | |
117 | 'apply_request', |
|
117 | 'apply_request', 'is_complete_request', | |
118 | ] |
|
118 | ] | |
119 | self.shell_handlers = {} |
|
119 | self.shell_handlers = {} | |
120 | for msg_type in msg_types: |
|
120 | for msg_type in msg_types: | |
@@ -480,6 +480,22 b' class Kernel(SingletonConfigurable):' | |||||
480 | """ |
|
480 | """ | |
481 | return {'status': 'ok', 'restart': restart} |
|
481 | return {'status': 'ok', 'restart': restart} | |
482 |
|
482 | |||
|
483 | def is_complete_request(self, stream, ident, parent): | |||
|
484 | content = parent['content'] | |||
|
485 | code = content['code'] | |||
|
486 | ||||
|
487 | reply_content = self.do_is_complete(code) | |||
|
488 | reply_content = json_clean(reply_content) | |||
|
489 | reply_msg = self.session.send(stream, 'is_complete_reply', | |||
|
490 | reply_content, parent, ident) | |||
|
491 | self.log.debug("%s", reply_msg) | |||
|
492 | ||||
|
493 | def do_is_complete(self, code): | |||
|
494 | """Override in subclasses to find completions. | |||
|
495 | """ | |||
|
496 | return {'status' : 'unknown', | |||
|
497 | } | |||
|
498 | ||||
483 | #--------------------------------------------------------------------------- |
|
499 | #--------------------------------------------------------------------------- | |
484 | # Engine methods |
|
500 | # Engine methods | |
485 | #--------------------------------------------------------------------------- |
|
501 | #--------------------------------------------------------------------------- |
@@ -573,6 +573,51 b' Message type: ``history_reply``::' | |||||
573 | 'history' : list, |
|
573 | 'history' : list, | |
574 | } |
|
574 | } | |
575 |
|
575 | |||
|
576 | .. _msging_is_complete: | |||
|
577 | ||||
|
578 | Code completeness | |||
|
579 | ----------------- | |||
|
580 | ||||
|
581 | .. versionadded:: 5.0 | |||
|
582 | ||||
|
583 | When the user enters a line in a console style interface, the console must | |||
|
584 | decide whether to immediately execute the current code, or whether to show a | |||
|
585 | continuation prompt for further input. For instance, in Python ``a = 5`` would | |||
|
586 | be executed immediately, while ``for i in range(5):`` would expect further input. | |||
|
587 | ||||
|
588 | There are four possible replies: | |||
|
589 | ||||
|
590 | - *complete* code is ready to be executed | |||
|
591 | - *incomplete* code should prompt for another line | |||
|
592 | - *invalid* code will typically be sent for execution, so that the user sees the | |||
|
593 | error soonest. | |||
|
594 | - *unknown* - if the kernel is not able to determine this. The frontend should | |||
|
595 | also handle the kernel not replying promptly. It may default to sending the | |||
|
596 | code for execution, or it may implement simple fallback heuristics for whether | |||
|
597 | to execute the code (e.g. execute after a blank line). | |||
|
598 | ||||
|
599 | Frontends may have ways to override this, forcing the code to be sent for | |||
|
600 | execution or forcing a continuation prompt. | |||
|
601 | ||||
|
602 | Message type: ``is_complete_request``:: | |||
|
603 | ||||
|
604 | content = { | |||
|
605 | # The code entered so far as a multiline string | |||
|
606 | 'code' : str, | |||
|
607 | } | |||
|
608 | ||||
|
609 | Message type: ``is_complete_reply``:: | |||
|
610 | ||||
|
611 | content = { | |||
|
612 | # One of 'complete', 'incomplete', 'invalid', 'unknown' | |||
|
613 | 'status' : str, | |||
|
614 | ||||
|
615 | # If status is 'incomplete', indent should contain the characters to use | |||
|
616 | # to indent the next line. This is only a hint: frontends may ignore it | |||
|
617 | # and use their own autoindentation rules. For other statuses, this | |||
|
618 | # field does not exist. | |||
|
619 | 'indent': str, | |||
|
620 | } | |||
576 |
|
621 | |||
577 | Connect |
|
622 | Connect | |
578 | ------- |
|
623 | ------- |
@@ -141,6 +141,17 b' relevant section of the :doc:`messaging spec <messaging>`.' | |||||
141 |
|
141 | |||
142 | :ref:`msging_history` messages |
|
142 | :ref:`msging_history` messages | |
143 |
|
143 | |||
|
144 | .. method:: do_is_complete(code) | |||
|
145 | ||||
|
146 | Is code entered in a console-like interface complete and ready to execute, | |||
|
147 | or should a continuation prompt be shown? | |||
|
148 | ||||
|
149 | :param str code: The code entered so far - possibly multiple lines | |||
|
150 | ||||
|
151 | .. seealso:: | |||
|
152 | ||||
|
153 | :ref:`msging_is_complete` messages | |||
|
154 | ||||
144 | .. method:: do_shutdown(restart) |
|
155 | .. method:: do_shutdown(restart) | |
145 |
|
156 | |||
146 | Shutdown the kernel. You only need to handle your own clean up - the kernel |
|
157 | Shutdown the kernel. You only need to handle your own clean up - the kernel |
General Comments 0
You need to be logged in to leave comments.
Login now