Show More
@@ -208,6 +208,8 b' class InputSplitter(object):' | |||
|
208 | 208 | _full_dedent = False |
|
209 | 209 | # Boolean indicating whether the current block is complete |
|
210 | 210 | _is_complete = None |
|
211 | # Boolean indicating whether the current block has an unrecoverable syntax error | |
|
212 | _is_invalid = False | |
|
211 | 213 | |
|
212 | 214 | def __init__(self): |
|
213 | 215 | """Create a new InputSplitter instance. |
@@ -223,6 +225,7 b' class InputSplitter(object):' | |||
|
223 | 225 | self.source = '' |
|
224 | 226 | self.code = None |
|
225 | 227 | self._is_complete = False |
|
228 | self._is_invalid = False | |
|
226 | 229 | self._full_dedent = False |
|
227 | 230 | |
|
228 | 231 | def source_reset(self): |
@@ -232,6 +235,42 b' class InputSplitter(object):' | |||
|
232 | 235 | self.reset() |
|
233 | 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 | 274 | def push(self, lines): |
|
236 | 275 | """Push one or more lines of input. |
|
237 | 276 | |
@@ -261,6 +300,7 b' class InputSplitter(object):' | |||
|
261 | 300 | # exception is raised in compilation, we don't mislead by having |
|
262 | 301 | # inconsistent code/source attributes. |
|
263 | 302 | self.code, self._is_complete = None, None |
|
303 | self._is_invalid = False | |
|
264 | 304 | |
|
265 | 305 | # Honor termination lines properly |
|
266 | 306 | if source.endswith('\\\n'): |
@@ -277,6 +317,7 b' class InputSplitter(object):' | |||
|
277 | 317 | except (SyntaxError, OverflowError, ValueError, TypeError, |
|
278 | 318 | MemoryError): |
|
279 | 319 | self._is_complete = True |
|
320 | self._is_invalid = True | |
|
280 | 321 | else: |
|
281 | 322 | # Compilation didn't produce any exceptions (though it may not have |
|
282 | 323 | # given a complete code object) |
@@ -342,6 +342,13 b' class InputSplitterTestCase(unittest.TestCase):' | |||
|
342 | 342 | isp.push(r"(1 \ ") |
|
343 | 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 | 352 | class InteractiveLoopTestCase(unittest.TestCase): |
|
346 | 353 | """Tests for an interactive loop like a python shell. |
|
347 | 354 | """ |
@@ -194,6 +194,7 b' class ShellChannel(ZMQSocketChannel):' | |||
|
194 | 194 | 'history', |
|
195 | 195 | 'kernel_info', |
|
196 | 196 | 'shutdown', |
|
197 | 'is_complete', | |
|
197 | 198 | ] |
|
198 | 199 | |
|
199 | 200 | def __init__(self, context, session, address): |
@@ -396,6 +397,10 b' class ShellChannel(ZMQSocketChannel):' | |||
|
396 | 397 | self._queue_send(msg) |
|
397 | 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 | 406 | class IOPubChannel(ZMQSocketChannel): |
@@ -205,3 +205,20 b' def test_help_output():' | |||
|
205 | 205 | """ipython kernel --help-all works""" |
|
206 | 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 | 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 | 175 | # IOPub messages |
|
164 | 176 | |
|
165 | 177 | class ExecuteInput(Reference): |
@@ -189,6 +201,7 b' references = {' | |||
|
189 | 201 | 'status' : Status(), |
|
190 | 202 | 'complete_reply' : CompleteReply(), |
|
191 | 203 | 'kernel_info_reply': KernelInfoReply(), |
|
204 | 'is_complete_reply': IsCompleteReply(), | |
|
192 | 205 | 'execute_input' : ExecuteInput(), |
|
193 | 206 | 'execute_result' : ExecuteResult(), |
|
194 | 207 | 'error' : Error(), |
@@ -382,6 +395,12 b' def test_single_payload():' | |||
|
382 | 395 | next_input_pls = [pl for pl in payload if pl["source"] == "set_next_input"] |
|
383 | 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 | 405 | # IOPub channel |
|
387 | 406 |
@@ -232,6 +232,13 b' class IPythonKernel(KernelBase):' | |||
|
232 | 232 | self.shell.exit_now = True |
|
233 | 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 | 242 | def do_apply(self, content, bufs, msg_id, reply_metadata): |
|
236 | 243 | shell = self.shell |
|
237 | 244 | try: |
@@ -114,7 +114,7 b' class Kernel(SingletonConfigurable):' | |||
|
114 | 114 | 'inspect_request', 'history_request', |
|
115 | 115 | 'kernel_info_request', |
|
116 | 116 | 'connect_request', 'shutdown_request', |
|
117 | 'apply_request', | |
|
117 | 'apply_request', 'is_complete_request', | |
|
118 | 118 | ] |
|
119 | 119 | self.shell_handlers = {} |
|
120 | 120 | for msg_type in msg_types: |
@@ -480,6 +480,22 b' class Kernel(SingletonConfigurable):' | |||
|
480 | 480 | """ |
|
481 | 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 | 500 | # Engine methods |
|
485 | 501 | #--------------------------------------------------------------------------- |
@@ -573,6 +573,51 b' Message type: ``history_reply``::' | |||
|
573 | 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 | 622 | Connect |
|
578 | 623 | ------- |
@@ -141,6 +141,17 b' relevant section of the :doc:`messaging spec <messaging>`.' | |||
|
141 | 141 | |
|
142 | 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 | 155 | .. method:: do_shutdown(restart) |
|
145 | 156 | |
|
146 | 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