##// END OF EJS Templates
Protect Qt console against SyntaxError from input transformers.
Thomas Kluyver -
Show More
@@ -1,661 +1,668 b''
1 """Analysis of text input into executable blocks.
1 """Analysis of text input into executable blocks.
2
2
3 The main class in this module, :class:`InputSplitter`, is designed to break
3 The main class in this module, :class:`InputSplitter`, is designed to break
4 input from either interactive, line-by-line environments or block-based ones,
4 input from either interactive, line-by-line environments or block-based ones,
5 into standalone blocks that can be executed by Python as 'single' statements
5 into standalone blocks that can be executed by Python as 'single' statements
6 (thus triggering sys.displayhook).
6 (thus triggering sys.displayhook).
7
7
8 A companion, :class:`IPythonInputSplitter`, provides the same functionality but
8 A companion, :class:`IPythonInputSplitter`, provides the same functionality but
9 with full support for the extended IPython syntax (magics, system calls, etc).
9 with full support for the extended IPython syntax (magics, system calls, etc).
10
10
11 For more details, see the class docstring below.
11 For more details, see the class docstring below.
12
12
13 Syntax Transformations
13 Syntax Transformations
14 ----------------------
14 ----------------------
15
15
16 One of the main jobs of the code in this file is to apply all syntax
16 One of the main jobs of the code in this file is to apply all syntax
17 transformations that make up 'the IPython language', i.e. magics, shell
17 transformations that make up 'the IPython language', i.e. magics, shell
18 escapes, etc. All transformations should be implemented as *fully stateless*
18 escapes, etc. All transformations should be implemented as *fully stateless*
19 entities, that simply take one line as their input and return a line.
19 entities, that simply take one line as their input and return a line.
20 Internally for implementation purposes they may be a normal function or a
20 Internally for implementation purposes they may be a normal function or a
21 callable object, but the only input they receive will be a single line and they
21 callable object, but the only input they receive will be a single line and they
22 should only return a line, without holding any data-dependent state between
22 should only return a line, without holding any data-dependent state between
23 calls.
23 calls.
24
24
25 As an example, the EscapedTransformer is a class so we can more clearly group
25 As an example, the EscapedTransformer is a class so we can more clearly group
26 together the functionality of dispatching to individual functions based on the
26 together the functionality of dispatching to individual functions based on the
27 starting escape character, but the only method for public use is its call
27 starting escape character, but the only method for public use is its call
28 method.
28 method.
29
29
30
30
31 ToDo
31 ToDo
32 ----
32 ----
33
33
34 - Should we make push() actually raise an exception once push_accepts_more()
34 - Should we make push() actually raise an exception once push_accepts_more()
35 returns False?
35 returns False?
36
36
37 - Naming cleanups. The tr_* names aren't the most elegant, though now they are
37 - Naming cleanups. The tr_* names aren't the most elegant, though now they are
38 at least just attributes of a class so not really very exposed.
38 at least just attributes of a class so not really very exposed.
39
39
40 - Think about the best way to support dynamic things: automagic, autocall,
40 - Think about the best way to support dynamic things: automagic, autocall,
41 macros, etc.
41 macros, etc.
42
42
43 - Think of a better heuristic for the application of the transforms in
43 - Think of a better heuristic for the application of the transforms in
44 IPythonInputSplitter.push() than looking at the buffer ending in ':'. Idea:
44 IPythonInputSplitter.push() than looking at the buffer ending in ':'. Idea:
45 track indentation change events (indent, dedent, nothing) and apply them only
45 track indentation change events (indent, dedent, nothing) and apply them only
46 if the indentation went up, but not otherwise.
46 if the indentation went up, but not otherwise.
47
47
48 - Think of the cleanest way for supporting user-specified transformations (the
48 - Think of the cleanest way for supporting user-specified transformations (the
49 user prefilters we had before).
49 user prefilters we had before).
50
50
51 Authors
51 Authors
52 -------
52 -------
53
53
54 * Fernando Perez
54 * Fernando Perez
55 * Brian Granger
55 * Brian Granger
56 """
56 """
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58 # Copyright (C) 2010 The IPython Development Team
58 # Copyright (C) 2010 The IPython Development Team
59 #
59 #
60 # Distributed under the terms of the BSD License. The full license is in
60 # Distributed under the terms of the BSD License. The full license is in
61 # the file COPYING, distributed as part of this software.
61 # the file COPYING, distributed as part of this software.
62 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
63
63
64 #-----------------------------------------------------------------------------
64 #-----------------------------------------------------------------------------
65 # Imports
65 # Imports
66 #-----------------------------------------------------------------------------
66 #-----------------------------------------------------------------------------
67 # stdlib
67 # stdlib
68 import ast
68 import ast
69 import codeop
69 import codeop
70 import re
70 import re
71 import sys
71 import sys
72
72
73 # IPython modules
73 # IPython modules
74 from IPython.utils.py3compat import cast_unicode
74 from IPython.utils.py3compat import cast_unicode
75 from IPython.core.inputtransformer import (leading_indent,
75 from IPython.core.inputtransformer import (leading_indent,
76 classic_prompt,
76 classic_prompt,
77 ipy_prompt,
77 ipy_prompt,
78 strip_encoding_cookie,
78 strip_encoding_cookie,
79 cellmagic,
79 cellmagic,
80 assemble_logical_lines,
80 assemble_logical_lines,
81 help_end,
81 help_end,
82 escaped_commands,
82 escaped_commands,
83 assign_from_magic,
83 assign_from_magic,
84 assign_from_system,
84 assign_from_system,
85 assemble_python_lines,
85 assemble_python_lines,
86 )
86 )
87
87
88 # These are available in this module for backwards compatibility.
88 # These are available in this module for backwards compatibility.
89 from IPython.core.inputtransformer import (ESC_SHELL, ESC_SH_CAP, ESC_HELP,
89 from IPython.core.inputtransformer import (ESC_SHELL, ESC_SH_CAP, ESC_HELP,
90 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,
90 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,
91 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN, ESC_SEQUENCES)
91 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN, ESC_SEQUENCES)
92
92
93 #-----------------------------------------------------------------------------
93 #-----------------------------------------------------------------------------
94 # Utilities
94 # Utilities
95 #-----------------------------------------------------------------------------
95 #-----------------------------------------------------------------------------
96
96
97 # FIXME: These are general-purpose utilities that later can be moved to the
97 # FIXME: These are general-purpose utilities that later can be moved to the
98 # general ward. Kept here for now because we're being very strict about test
98 # general ward. Kept here for now because we're being very strict about test
99 # coverage with this code, and this lets us ensure that we keep 100% coverage
99 # coverage with this code, and this lets us ensure that we keep 100% coverage
100 # while developing.
100 # while developing.
101
101
102 # compiled regexps for autoindent management
102 # compiled regexps for autoindent management
103 dedent_re = re.compile('|'.join([
103 dedent_re = re.compile('|'.join([
104 r'^\s+raise(\s.*)?$', # raise statement (+ space + other stuff, maybe)
104 r'^\s+raise(\s.*)?$', # raise statement (+ space + other stuff, maybe)
105 r'^\s+raise\([^\)]*\).*$', # wacky raise with immediate open paren
105 r'^\s+raise\([^\)]*\).*$', # wacky raise with immediate open paren
106 r'^\s+return(\s.*)?$', # normal return (+ space + other stuff, maybe)
106 r'^\s+return(\s.*)?$', # normal return (+ space + other stuff, maybe)
107 r'^\s+return\([^\)]*\).*$', # wacky return with immediate open paren
107 r'^\s+return\([^\)]*\).*$', # wacky return with immediate open paren
108 r'^\s+pass\s*$', # pass (optionally followed by trailing spaces)
108 r'^\s+pass\s*$', # pass (optionally followed by trailing spaces)
109 r'^\s+break\s*$', # break (optionally followed by trailing spaces)
109 r'^\s+break\s*$', # break (optionally followed by trailing spaces)
110 r'^\s+continue\s*$', # continue (optionally followed by trailing spaces)
110 r'^\s+continue\s*$', # continue (optionally followed by trailing spaces)
111 ]))
111 ]))
112 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
112 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
113
113
114 # regexp to match pure comment lines so we don't accidentally insert 'if 1:'
114 # regexp to match pure comment lines so we don't accidentally insert 'if 1:'
115 # before pure comments
115 # before pure comments
116 comment_line_re = re.compile('^\s*\#')
116 comment_line_re = re.compile('^\s*\#')
117
117
118
118
119 def num_ini_spaces(s):
119 def num_ini_spaces(s):
120 """Return the number of initial spaces in a string.
120 """Return the number of initial spaces in a string.
121
121
122 Note that tabs are counted as a single space. For now, we do *not* support
122 Note that tabs are counted as a single space. For now, we do *not* support
123 mixing of tabs and spaces in the user's input.
123 mixing of tabs and spaces in the user's input.
124
124
125 Parameters
125 Parameters
126 ----------
126 ----------
127 s : string
127 s : string
128
128
129 Returns
129 Returns
130 -------
130 -------
131 n : int
131 n : int
132 """
132 """
133
133
134 ini_spaces = ini_spaces_re.match(s)
134 ini_spaces = ini_spaces_re.match(s)
135 if ini_spaces:
135 if ini_spaces:
136 return ini_spaces.end()
136 return ini_spaces.end()
137 else:
137 else:
138 return 0
138 return 0
139
139
140 def last_blank(src):
140 def last_blank(src):
141 """Determine if the input source ends in a blank.
141 """Determine if the input source ends in a blank.
142
142
143 A blank is either a newline or a line consisting of whitespace.
143 A blank is either a newline or a line consisting of whitespace.
144
144
145 Parameters
145 Parameters
146 ----------
146 ----------
147 src : string
147 src : string
148 A single or multiline string.
148 A single or multiline string.
149 """
149 """
150 if not src: return False
150 if not src: return False
151 ll = src.splitlines()[-1]
151 ll = src.splitlines()[-1]
152 return (ll == '') or ll.isspace()
152 return (ll == '') or ll.isspace()
153
153
154
154
155 last_two_blanks_re = re.compile(r'\n\s*\n\s*$', re.MULTILINE)
155 last_two_blanks_re = re.compile(r'\n\s*\n\s*$', re.MULTILINE)
156 last_two_blanks_re2 = re.compile(r'.+\n\s*\n\s+$', re.MULTILINE)
156 last_two_blanks_re2 = re.compile(r'.+\n\s*\n\s+$', re.MULTILINE)
157
157
158 def last_two_blanks(src):
158 def last_two_blanks(src):
159 """Determine if the input source ends in two blanks.
159 """Determine if the input source ends in two blanks.
160
160
161 A blank is either a newline or a line consisting of whitespace.
161 A blank is either a newline or a line consisting of whitespace.
162
162
163 Parameters
163 Parameters
164 ----------
164 ----------
165 src : string
165 src : string
166 A single or multiline string.
166 A single or multiline string.
167 """
167 """
168 if not src: return False
168 if not src: return False
169 # The logic here is tricky: I couldn't get a regexp to work and pass all
169 # The logic here is tricky: I couldn't get a regexp to work and pass all
170 # the tests, so I took a different approach: split the source by lines,
170 # the tests, so I took a different approach: split the source by lines,
171 # grab the last two and prepend '###\n' as a stand-in for whatever was in
171 # grab the last two and prepend '###\n' as a stand-in for whatever was in
172 # the body before the last two lines. Then, with that structure, it's
172 # the body before the last two lines. Then, with that structure, it's
173 # possible to analyze with two regexps. Not the most elegant solution, but
173 # possible to analyze with two regexps. Not the most elegant solution, but
174 # it works. If anyone tries to change this logic, make sure to validate
174 # it works. If anyone tries to change this logic, make sure to validate
175 # the whole test suite first!
175 # the whole test suite first!
176 new_src = '\n'.join(['###\n'] + src.splitlines()[-2:])
176 new_src = '\n'.join(['###\n'] + src.splitlines()[-2:])
177 return (bool(last_two_blanks_re.match(new_src)) or
177 return (bool(last_two_blanks_re.match(new_src)) or
178 bool(last_two_blanks_re2.match(new_src)) )
178 bool(last_two_blanks_re2.match(new_src)) )
179
179
180
180
181 def remove_comments(src):
181 def remove_comments(src):
182 """Remove all comments from input source.
182 """Remove all comments from input source.
183
183
184 Note: comments are NOT recognized inside of strings!
184 Note: comments are NOT recognized inside of strings!
185
185
186 Parameters
186 Parameters
187 ----------
187 ----------
188 src : string
188 src : string
189 A single or multiline input string.
189 A single or multiline input string.
190
190
191 Returns
191 Returns
192 -------
192 -------
193 String with all Python comments removed.
193 String with all Python comments removed.
194 """
194 """
195
195
196 return re.sub('#.*', '', src)
196 return re.sub('#.*', '', src)
197
197
198
198
199 def get_input_encoding():
199 def get_input_encoding():
200 """Return the default standard input encoding.
200 """Return the default standard input encoding.
201
201
202 If sys.stdin has no encoding, 'ascii' is returned."""
202 If sys.stdin has no encoding, 'ascii' is returned."""
203 # There are strange environments for which sys.stdin.encoding is None. We
203 # There are strange environments for which sys.stdin.encoding is None. We
204 # ensure that a valid encoding is returned.
204 # ensure that a valid encoding is returned.
205 encoding = getattr(sys.stdin, 'encoding', None)
205 encoding = getattr(sys.stdin, 'encoding', None)
206 if encoding is None:
206 if encoding is None:
207 encoding = 'ascii'
207 encoding = 'ascii'
208 return encoding
208 return encoding
209
209
210 #-----------------------------------------------------------------------------
210 #-----------------------------------------------------------------------------
211 # Classes and functions for normal Python syntax handling
211 # Classes and functions for normal Python syntax handling
212 #-----------------------------------------------------------------------------
212 #-----------------------------------------------------------------------------
213
213
214 class InputSplitter(object):
214 class InputSplitter(object):
215 """An object that can accumulate lines of Python source before execution.
215 """An object that can accumulate lines of Python source before execution.
216
216
217 This object is designed to be fed python source line-by-line, using
217 This object is designed to be fed python source line-by-line, using
218 :meth:`push`. It will return on each push whether the currently pushed
218 :meth:`push`. It will return on each push whether the currently pushed
219 code could be executed already. In addition, it provides a method called
219 code could be executed already. In addition, it provides a method called
220 :meth:`push_accepts_more` that can be used to query whether more input
220 :meth:`push_accepts_more` that can be used to query whether more input
221 can be pushed into a single interactive block.
221 can be pushed into a single interactive block.
222
222
223 This is a simple example of how an interactive terminal-based client can use
223 This is a simple example of how an interactive terminal-based client can use
224 this tool::
224 this tool::
225
225
226 isp = InputSplitter()
226 isp = InputSplitter()
227 while isp.push_accepts_more():
227 while isp.push_accepts_more():
228 indent = ' '*isp.indent_spaces
228 indent = ' '*isp.indent_spaces
229 prompt = '>>> ' + indent
229 prompt = '>>> ' + indent
230 line = indent + raw_input(prompt)
230 line = indent + raw_input(prompt)
231 isp.push(line)
231 isp.push(line)
232 print 'Input source was:\n', isp.source_reset(),
232 print 'Input source was:\n', isp.source_reset(),
233 """
233 """
234 # Number of spaces of indentation computed from input that has been pushed
234 # Number of spaces of indentation computed from input that has been pushed
235 # so far. This is the attributes callers should query to get the current
235 # so far. This is the attributes callers should query to get the current
236 # indentation level, in order to provide auto-indent facilities.
236 # indentation level, in order to provide auto-indent facilities.
237 indent_spaces = 0
237 indent_spaces = 0
238 # String, indicating the default input encoding. It is computed by default
238 # String, indicating the default input encoding. It is computed by default
239 # at initialization time via get_input_encoding(), but it can be reset by a
239 # at initialization time via get_input_encoding(), but it can be reset by a
240 # client with specific knowledge of the encoding.
240 # client with specific knowledge of the encoding.
241 encoding = ''
241 encoding = ''
242 # String where the current full source input is stored, properly encoded.
242 # String where the current full source input is stored, properly encoded.
243 # Reading this attribute is the normal way of querying the currently pushed
243 # Reading this attribute is the normal way of querying the currently pushed
244 # source code, that has been properly encoded.
244 # source code, that has been properly encoded.
245 source = ''
245 source = ''
246 # Code object corresponding to the current source. It is automatically
246 # Code object corresponding to the current source. It is automatically
247 # synced to the source, so it can be queried at any time to obtain the code
247 # synced to the source, so it can be queried at any time to obtain the code
248 # object; it will be None if the source doesn't compile to valid Python.
248 # object; it will be None if the source doesn't compile to valid Python.
249 code = None
249 code = None
250
250
251 # Private attributes
251 # Private attributes
252
252
253 # List with lines of input accumulated so far
253 # List with lines of input accumulated so far
254 _buffer = None
254 _buffer = None
255 # Command compiler
255 # Command compiler
256 _compile = None
256 _compile = None
257 # Mark when input has changed indentation all the way back to flush-left
257 # Mark when input has changed indentation all the way back to flush-left
258 _full_dedent = False
258 _full_dedent = False
259 # Boolean indicating whether the current block is complete
259 # Boolean indicating whether the current block is complete
260 _is_complete = None
260 _is_complete = None
261
261
262 def __init__(self):
262 def __init__(self):
263 """Create a new InputSplitter instance.
263 """Create a new InputSplitter instance.
264 """
264 """
265 self._buffer = []
265 self._buffer = []
266 self._compile = codeop.CommandCompiler()
266 self._compile = codeop.CommandCompiler()
267 self.encoding = get_input_encoding()
267 self.encoding = get_input_encoding()
268
268
269 def reset(self):
269 def reset(self):
270 """Reset the input buffer and associated state."""
270 """Reset the input buffer and associated state."""
271 self.indent_spaces = 0
271 self.indent_spaces = 0
272 self._buffer[:] = []
272 self._buffer[:] = []
273 self.source = ''
273 self.source = ''
274 self.code = None
274 self.code = None
275 self._is_complete = False
275 self._is_complete = False
276 self._full_dedent = False
276 self._full_dedent = False
277
277
278 def source_reset(self):
278 def source_reset(self):
279 """Return the input source and perform a full reset.
279 """Return the input source and perform a full reset.
280 """
280 """
281 out = self.source
281 out = self.source
282 self.reset()
282 self.reset()
283 return out
283 return out
284
284
285 def push(self, lines):
285 def push(self, lines):
286 """Push one or more lines of input.
286 """Push one or more lines of input.
287
287
288 This stores the given lines and returns a status code indicating
288 This stores the given lines and returns a status code indicating
289 whether the code forms a complete Python block or not.
289 whether the code forms a complete Python block or not.
290
290
291 Any exceptions generated in compilation are swallowed, but if an
291 Any exceptions generated in compilation are swallowed, but if an
292 exception was produced, the method returns True.
292 exception was produced, the method returns True.
293
293
294 Parameters
294 Parameters
295 ----------
295 ----------
296 lines : string
296 lines : string
297 One or more lines of Python input.
297 One or more lines of Python input.
298
298
299 Returns
299 Returns
300 -------
300 -------
301 is_complete : boolean
301 is_complete : boolean
302 True if the current input source (the result of the current input
302 True if the current input source (the result of the current input
303 plus prior inputs) forms a complete Python execution block. Note that
303 plus prior inputs) forms a complete Python execution block. Note that
304 this value is also stored as a private attribute (``_is_complete``), so it
304 this value is also stored as a private attribute (``_is_complete``), so it
305 can be queried at any time.
305 can be queried at any time.
306 """
306 """
307 self._store(lines)
307 self._store(lines)
308 source = self.source
308 source = self.source
309
309
310 # Before calling _compile(), reset the code object to None so that if an
310 # Before calling _compile(), reset the code object to None so that if an
311 # exception is raised in compilation, we don't mislead by having
311 # exception is raised in compilation, we don't mislead by having
312 # inconsistent code/source attributes.
312 # inconsistent code/source attributes.
313 self.code, self._is_complete = None, None
313 self.code, self._is_complete = None, None
314
314
315 # Honor termination lines properly
315 # Honor termination lines properly
316 if source.endswith('\\\n'):
316 if source.endswith('\\\n'):
317 return False
317 return False
318
318
319 self._update_indent(lines)
319 self._update_indent(lines)
320 try:
320 try:
321 self.code = self._compile(source, symbol="exec")
321 self.code = self._compile(source, symbol="exec")
322 # Invalid syntax can produce any of a number of different errors from
322 # Invalid syntax can produce any of a number of different errors from
323 # inside the compiler, so we have to catch them all. Syntax errors
323 # inside the compiler, so we have to catch them all. Syntax errors
324 # immediately produce a 'ready' block, so the invalid Python can be
324 # immediately produce a 'ready' block, so the invalid Python can be
325 # sent to the kernel for evaluation with possible ipython
325 # sent to the kernel for evaluation with possible ipython
326 # special-syntax conversion.
326 # special-syntax conversion.
327 except (SyntaxError, OverflowError, ValueError, TypeError,
327 except (SyntaxError, OverflowError, ValueError, TypeError,
328 MemoryError):
328 MemoryError):
329 self._is_complete = True
329 self._is_complete = True
330 else:
330 else:
331 # Compilation didn't produce any exceptions (though it may not have
331 # Compilation didn't produce any exceptions (though it may not have
332 # given a complete code object)
332 # given a complete code object)
333 self._is_complete = self.code is not None
333 self._is_complete = self.code is not None
334
334
335 return self._is_complete
335 return self._is_complete
336
336
337 def push_accepts_more(self):
337 def push_accepts_more(self):
338 """Return whether a block of interactive input can accept more input.
338 """Return whether a block of interactive input can accept more input.
339
339
340 This method is meant to be used by line-oriented frontends, who need to
340 This method is meant to be used by line-oriented frontends, who need to
341 guess whether a block is complete or not based solely on prior and
341 guess whether a block is complete or not based solely on prior and
342 current input lines. The InputSplitter considers it has a complete
342 current input lines. The InputSplitter considers it has a complete
343 interactive block and will not accept more input when either:
343 interactive block and will not accept more input when either:
344
344
345 * A SyntaxError is raised
345 * A SyntaxError is raised
346
346
347 * The code is complete and consists of a single line or a single
347 * The code is complete and consists of a single line or a single
348 non-compound statement
348 non-compound statement
349
349
350 * The code is complete and has a blank line at the end
350 * The code is complete and has a blank line at the end
351
351
352 If the current input produces a syntax error, this method immediately
352 If the current input produces a syntax error, this method immediately
353 returns False but does *not* raise the syntax error exception, as
353 returns False but does *not* raise the syntax error exception, as
354 typically clients will want to send invalid syntax to an execution
354 typically clients will want to send invalid syntax to an execution
355 backend which might convert the invalid syntax into valid Python via
355 backend which might convert the invalid syntax into valid Python via
356 one of the dynamic IPython mechanisms.
356 one of the dynamic IPython mechanisms.
357 """
357 """
358
358
359 # With incomplete input, unconditionally accept more
359 # With incomplete input, unconditionally accept more
360 # A syntax error also sets _is_complete to True - see push()
360 # A syntax error also sets _is_complete to True - see push()
361 if not self._is_complete:
361 if not self._is_complete:
362 #print("Not complete") # debug
362 #print("Not complete") # debug
363 return True
363 return True
364
364
365 # The user can make any (complete) input execute by leaving a blank line
365 # The user can make any (complete) input execute by leaving a blank line
366 last_line = self.source.splitlines()[-1]
366 last_line = self.source.splitlines()[-1]
367 if (not last_line) or last_line.isspace():
367 if (not last_line) or last_line.isspace():
368 #print("Blank line") # debug
368 #print("Blank line") # debug
369 return False
369 return False
370
370
371 # If there's just a single line or AST node, and we're flush left, as is
371 # If there's just a single line or AST node, and we're flush left, as is
372 # the case after a simple statement such as 'a=1', we want to execute it
372 # the case after a simple statement such as 'a=1', we want to execute it
373 # straight away.
373 # straight away.
374 if self.indent_spaces==0:
374 if self.indent_spaces==0:
375 if len(self.source.splitlines()) <= 1:
375 if len(self.source.splitlines()) <= 1:
376 return False
376 return False
377
377
378 try:
378 try:
379 code_ast = ast.parse(u''.join(self._buffer))
379 code_ast = ast.parse(u''.join(self._buffer))
380 except Exception:
380 except Exception:
381 #print("Can't parse AST") # debug
381 #print("Can't parse AST") # debug
382 return False
382 return False
383 else:
383 else:
384 if len(code_ast.body) == 1 and \
384 if len(code_ast.body) == 1 and \
385 not hasattr(code_ast.body[0], 'body'):
385 not hasattr(code_ast.body[0], 'body'):
386 #print("Simple statement") # debug
386 #print("Simple statement") # debug
387 return False
387 return False
388
388
389 # General fallback - accept more code
389 # General fallback - accept more code
390 return True
390 return True
391
391
392 #------------------------------------------------------------------------
392 #------------------------------------------------------------------------
393 # Private interface
393 # Private interface
394 #------------------------------------------------------------------------
394 #------------------------------------------------------------------------
395
395
396 def _find_indent(self, line):
396 def _find_indent(self, line):
397 """Compute the new indentation level for a single line.
397 """Compute the new indentation level for a single line.
398
398
399 Parameters
399 Parameters
400 ----------
400 ----------
401 line : str
401 line : str
402 A single new line of non-whitespace, non-comment Python input.
402 A single new line of non-whitespace, non-comment Python input.
403
403
404 Returns
404 Returns
405 -------
405 -------
406 indent_spaces : int
406 indent_spaces : int
407 New value for the indent level (it may be equal to self.indent_spaces
407 New value for the indent level (it may be equal to self.indent_spaces
408 if indentation doesn't change.
408 if indentation doesn't change.
409
409
410 full_dedent : boolean
410 full_dedent : boolean
411 Whether the new line causes a full flush-left dedent.
411 Whether the new line causes a full flush-left dedent.
412 """
412 """
413 indent_spaces = self.indent_spaces
413 indent_spaces = self.indent_spaces
414 full_dedent = self._full_dedent
414 full_dedent = self._full_dedent
415
415
416 inisp = num_ini_spaces(line)
416 inisp = num_ini_spaces(line)
417 if inisp < indent_spaces:
417 if inisp < indent_spaces:
418 indent_spaces = inisp
418 indent_spaces = inisp
419 if indent_spaces <= 0:
419 if indent_spaces <= 0:
420 #print 'Full dedent in text',self.source # dbg
420 #print 'Full dedent in text',self.source # dbg
421 full_dedent = True
421 full_dedent = True
422
422
423 if line.rstrip()[-1] == ':':
423 if line.rstrip()[-1] == ':':
424 indent_spaces += 4
424 indent_spaces += 4
425 elif dedent_re.match(line):
425 elif dedent_re.match(line):
426 indent_spaces -= 4
426 indent_spaces -= 4
427 if indent_spaces <= 0:
427 if indent_spaces <= 0:
428 full_dedent = True
428 full_dedent = True
429
429
430 # Safety
430 # Safety
431 if indent_spaces < 0:
431 if indent_spaces < 0:
432 indent_spaces = 0
432 indent_spaces = 0
433 #print 'safety' # dbg
433 #print 'safety' # dbg
434
434
435 return indent_spaces, full_dedent
435 return indent_spaces, full_dedent
436
436
437 def _update_indent(self, lines):
437 def _update_indent(self, lines):
438 for line in remove_comments(lines).splitlines():
438 for line in remove_comments(lines).splitlines():
439 if line and not line.isspace():
439 if line and not line.isspace():
440 self.indent_spaces, self._full_dedent = self._find_indent(line)
440 self.indent_spaces, self._full_dedent = self._find_indent(line)
441
441
442 def _store(self, lines, buffer=None, store='source'):
442 def _store(self, lines, buffer=None, store='source'):
443 """Store one or more lines of input.
443 """Store one or more lines of input.
444
444
445 If input lines are not newline-terminated, a newline is automatically
445 If input lines are not newline-terminated, a newline is automatically
446 appended."""
446 appended."""
447
447
448 if buffer is None:
448 if buffer is None:
449 buffer = self._buffer
449 buffer = self._buffer
450
450
451 if lines.endswith('\n'):
451 if lines.endswith('\n'):
452 buffer.append(lines)
452 buffer.append(lines)
453 else:
453 else:
454 buffer.append(lines+'\n')
454 buffer.append(lines+'\n')
455 setattr(self, store, self._set_source(buffer))
455 setattr(self, store, self._set_source(buffer))
456
456
457 def _set_source(self, buffer):
457 def _set_source(self, buffer):
458 return u''.join(buffer)
458 return u''.join(buffer)
459
459
460
460
461 class IPythonInputSplitter(InputSplitter):
461 class IPythonInputSplitter(InputSplitter):
462 """An input splitter that recognizes all of IPython's special syntax."""
462 """An input splitter that recognizes all of IPython's special syntax."""
463
463
464 # String with raw, untransformed input.
464 # String with raw, untransformed input.
465 source_raw = ''
465 source_raw = ''
466
466
467 # Flag to track when a transformer has stored input that it hasn't given
467 # Flag to track when a transformer has stored input that it hasn't given
468 # back yet.
468 # back yet.
469 transformer_accumulating = False
469 transformer_accumulating = False
470
470
471 # Flag to track when assemble_python_lines has stored input that it hasn't
471 # Flag to track when assemble_python_lines has stored input that it hasn't
472 # given back yet.
472 # given back yet.
473 within_python_line = False
473 within_python_line = False
474
474
475 # Private attributes
475 # Private attributes
476
476
477 # List with lines of raw input accumulated so far.
477 # List with lines of raw input accumulated so far.
478 _buffer_raw = None
478 _buffer_raw = None
479
479
480 def __init__(self, line_input_checker=True, physical_line_transforms=None,
480 def __init__(self, line_input_checker=True, physical_line_transforms=None,
481 logical_line_transforms=None, python_line_transforms=None):
481 logical_line_transforms=None, python_line_transforms=None):
482 super(IPythonInputSplitter, self).__init__()
482 super(IPythonInputSplitter, self).__init__()
483 self._buffer_raw = []
483 self._buffer_raw = []
484 self._validate = True
484 self._validate = True
485
485
486 if physical_line_transforms is not None:
486 if physical_line_transforms is not None:
487 self.physical_line_transforms = physical_line_transforms
487 self.physical_line_transforms = physical_line_transforms
488 else:
488 else:
489 self.physical_line_transforms = [
489 self.physical_line_transforms = [
490 leading_indent(),
490 leading_indent(),
491 classic_prompt(),
491 classic_prompt(),
492 ipy_prompt(),
492 ipy_prompt(),
493 strip_encoding_cookie(),
493 strip_encoding_cookie(),
494 cellmagic(end_on_blank_line=line_input_checker),
494 cellmagic(end_on_blank_line=line_input_checker),
495 ]
495 ]
496
496
497 self.assemble_logical_lines = assemble_logical_lines()
497 self.assemble_logical_lines = assemble_logical_lines()
498 if logical_line_transforms is not None:
498 if logical_line_transforms is not None:
499 self.logical_line_transforms = logical_line_transforms
499 self.logical_line_transforms = logical_line_transforms
500 else:
500 else:
501 self.logical_line_transforms = [
501 self.logical_line_transforms = [
502 help_end(),
502 help_end(),
503 escaped_commands(),
503 escaped_commands(),
504 assign_from_magic(),
504 assign_from_magic(),
505 assign_from_system(),
505 assign_from_system(),
506 ]
506 ]
507
507
508 self.assemble_python_lines = assemble_python_lines()
508 self.assemble_python_lines = assemble_python_lines()
509 if python_line_transforms is not None:
509 if python_line_transforms is not None:
510 self.python_line_transforms = python_line_transforms
510 self.python_line_transforms = python_line_transforms
511 else:
511 else:
512 # We don't use any of these at present
512 # We don't use any of these at present
513 self.python_line_transforms = []
513 self.python_line_transforms = []
514
514
515 @property
515 @property
516 def transforms(self):
516 def transforms(self):
517 "Quick access to all transformers."
517 "Quick access to all transformers."
518 return self.physical_line_transforms + \
518 return self.physical_line_transforms + \
519 [self.assemble_logical_lines] + self.logical_line_transforms + \
519 [self.assemble_logical_lines] + self.logical_line_transforms + \
520 [self.assemble_python_lines] + self.python_line_transforms
520 [self.assemble_python_lines] + self.python_line_transforms
521
521
522 @property
522 @property
523 def transforms_in_use(self):
523 def transforms_in_use(self):
524 """Transformers, excluding logical line transformers if we're in a
524 """Transformers, excluding logical line transformers if we're in a
525 Python line."""
525 Python line."""
526 t = self.physical_line_transforms[:]
526 t = self.physical_line_transforms[:]
527 if not self.within_python_line:
527 if not self.within_python_line:
528 t += [self.assemble_logical_lines] + self.logical_line_transforms
528 t += [self.assemble_logical_lines] + self.logical_line_transforms
529 return t + [self.assemble_python_lines] + self.python_line_transforms
529 return t + [self.assemble_python_lines] + self.python_line_transforms
530
530
531 def reset(self):
531 def reset(self):
532 """Reset the input buffer and associated state."""
532 """Reset the input buffer and associated state."""
533 super(IPythonInputSplitter, self).reset()
533 super(IPythonInputSplitter, self).reset()
534 self._buffer_raw[:] = []
534 self._buffer_raw[:] = []
535 self.source_raw = ''
535 self.source_raw = ''
536 self.transformer_accumulating = False
536 self.transformer_accumulating = False
537 self.within_python_line = False
537 self.within_python_line = False
538
539 last_exc = None
538 for t in self.transforms:
540 for t in self.transforms:
539 t.reset()
541 try:
542 t.reset()
543 except SyntaxError as e:
544 last_exc = e
545 if last_exc is not None:
546 raise last_exc
540
547
541 def flush_transformers(self):
548 def flush_transformers(self):
542 def _flush(transform, out):
549 def _flush(transform, out):
543 if out is not None:
550 if out is not None:
544 tmp = transform.push(out)
551 tmp = transform.push(out)
545 return tmp or transform.reset() or None
552 return tmp or transform.reset() or None
546 else:
553 else:
547 return transform.reset() or None
554 return transform.reset() or None
548
555
549 out = None
556 out = None
550 for t in self.transforms_in_use:
557 for t in self.transforms_in_use:
551 out = _flush(t, out)
558 out = _flush(t, out)
552
559
553 if out is not None:
560 if out is not None:
554 self._store(out)
561 self._store(out)
555
562
556 def source_raw_reset(self):
563 def source_raw_reset(self):
557 """Return input and raw source and perform a full reset.
564 """Return input and raw source and perform a full reset.
558 """
565 """
559 self.flush_transformers()
566 self.flush_transformers()
560 out = self.source
567 out = self.source
561 out_r = self.source_raw
568 out_r = self.source_raw
562 self.reset()
569 self.reset()
563 return out, out_r
570 return out, out_r
564
571
565 def source_reset(self):
572 def source_reset(self):
566 self.flush_transformers()
573 self.flush_transformers()
567 return super(IPythonInputSplitter, self).source_reset()
574 return super(IPythonInputSplitter, self).source_reset()
568
575
569 def push_accepts_more(self):
576 def push_accepts_more(self):
570 if self.transformer_accumulating:
577 if self.transformer_accumulating:
571 return True
578 return True
572 else:
579 else:
573 return super(IPythonInputSplitter, self).push_accepts_more()
580 return super(IPythonInputSplitter, self).push_accepts_more()
574
581
575 def transform_cell(self, cell):
582 def transform_cell(self, cell):
576 """Process and translate a cell of input.
583 """Process and translate a cell of input.
577 """
584 """
578 self.reset()
585 self.reset()
579 self.push(cell)
586 self.push(cell)
580 return self.source_reset()
587 return self.source_reset()
581
588
582 def push(self, lines):
589 def push(self, lines):
583 """Push one or more lines of IPython input.
590 """Push one or more lines of IPython input.
584
591
585 This stores the given lines and returns a status code indicating
592 This stores the given lines and returns a status code indicating
586 whether the code forms a complete Python block or not, after processing
593 whether the code forms a complete Python block or not, after processing
587 all input lines for special IPython syntax.
594 all input lines for special IPython syntax.
588
595
589 Any exceptions generated in compilation are swallowed, but if an
596 Any exceptions generated in compilation are swallowed, but if an
590 exception was produced, the method returns True.
597 exception was produced, the method returns True.
591
598
592 Parameters
599 Parameters
593 ----------
600 ----------
594 lines : string
601 lines : string
595 One or more lines of Python input.
602 One or more lines of Python input.
596
603
597 Returns
604 Returns
598 -------
605 -------
599 is_complete : boolean
606 is_complete : boolean
600 True if the current input source (the result of the current input
607 True if the current input source (the result of the current input
601 plus prior inputs) forms a complete Python execution block. Note that
608 plus prior inputs) forms a complete Python execution block. Note that
602 this value is also stored as a private attribute (_is_complete), so it
609 this value is also stored as a private attribute (_is_complete), so it
603 can be queried at any time.
610 can be queried at any time.
604 """
611 """
605
612
606 # We must ensure all input is pure unicode
613 # We must ensure all input is pure unicode
607 lines = cast_unicode(lines, self.encoding)
614 lines = cast_unicode(lines, self.encoding)
608
615
609 # ''.splitlines() --> [], but we need to push the empty line to transformers
616 # ''.splitlines() --> [], but we need to push the empty line to transformers
610 lines_list = lines.splitlines()
617 lines_list = lines.splitlines()
611 if not lines_list:
618 if not lines_list:
612 lines_list = ['']
619 lines_list = ['']
613
620
614 # Store raw source before applying any transformations to it. Note
621 # Store raw source before applying any transformations to it. Note
615 # that this must be done *after* the reset() call that would otherwise
622 # that this must be done *after* the reset() call that would otherwise
616 # flush the buffer.
623 # flush the buffer.
617 self._store(lines, self._buffer_raw, 'source_raw')
624 self._store(lines, self._buffer_raw, 'source_raw')
618
625
619 for line in lines_list:
626 for line in lines_list:
620 out = self.push_line(line)
627 out = self.push_line(line)
621
628
622 return out
629 return out
623
630
624 def push_line(self, line):
631 def push_line(self, line):
625 buf = self._buffer
632 buf = self._buffer
626
633
627 def _accumulating(dbg):
634 def _accumulating(dbg):
628 #print(dbg)
635 #print(dbg)
629 self.transformer_accumulating = True
636 self.transformer_accumulating = True
630 return False
637 return False
631
638
632 for transformer in self.physical_line_transforms:
639 for transformer in self.physical_line_transforms:
633 line = transformer.push(line)
640 line = transformer.push(line)
634 if line is None:
641 if line is None:
635 return _accumulating(transformer)
642 return _accumulating(transformer)
636
643
637 if not self.within_python_line:
644 if not self.within_python_line:
638 line = self.assemble_logical_lines.push(line)
645 line = self.assemble_logical_lines.push(line)
639 if line is None:
646 if line is None:
640 return _accumulating('acc logical line')
647 return _accumulating('acc logical line')
641
648
642 for transformer in self.logical_line_transforms:
649 for transformer in self.logical_line_transforms:
643 line = transformer.push(line)
650 line = transformer.push(line)
644 if line is None:
651 if line is None:
645 return _accumulating(transformer)
652 return _accumulating(transformer)
646
653
647 line = self.assemble_python_lines.push(line)
654 line = self.assemble_python_lines.push(line)
648 if line is None:
655 if line is None:
649 self.within_python_line = True
656 self.within_python_line = True
650 return _accumulating('acc python line')
657 return _accumulating('acc python line')
651 else:
658 else:
652 self.within_python_line = False
659 self.within_python_line = False
653
660
654 for transformer in self.python_line_transforms:
661 for transformer in self.python_line_transforms:
655 line = transformer.push(line)
662 line = transformer.push(line)
656 if line is None:
663 if line is None:
657 return _accumulating(transformer)
664 return _accumulating(transformer)
658
665
659 #print("transformers clear") #debug
666 #print("transformers clear") #debug
660 self.transformer_accumulating = False
667 self.transformer_accumulating = False
661 return super(IPythonInputSplitter, self).push(line)
668 return super(IPythonInputSplitter, self).push(line)
@@ -1,784 +1,793 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import uuid
6 import uuid
7
7
8 # System library imports
8 # System library imports
9 from pygments.lexers import PythonLexer
9 from pygments.lexers import PythonLexer
10 from IPython.external import qt
10 from IPython.external import qt
11 from IPython.external.qt import QtCore, QtGui
11 from IPython.external.qt import QtCore, QtGui
12
12
13 # Local imports
13 # Local imports
14 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
14 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
15 from IPython.core.inputtransformer import classic_prompt
15 from IPython.core.inputtransformer import classic_prompt
16 from IPython.core.oinspect import call_tip
16 from IPython.core.oinspect import call_tip
17 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
17 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
18 from IPython.utils.traitlets import Bool, Instance, Unicode
18 from IPython.utils.traitlets import Bool, Instance, Unicode
19 from .bracket_matcher import BracketMatcher
19 from .bracket_matcher import BracketMatcher
20 from .call_tip_widget import CallTipWidget
20 from .call_tip_widget import CallTipWidget
21 from .completion_lexer import CompletionLexer
21 from .completion_lexer import CompletionLexer
22 from .history_console_widget import HistoryConsoleWidget
22 from .history_console_widget import HistoryConsoleWidget
23 from .pygments_highlighter import PygmentsHighlighter
23 from .pygments_highlighter import PygmentsHighlighter
24
24
25
25
26 class FrontendHighlighter(PygmentsHighlighter):
26 class FrontendHighlighter(PygmentsHighlighter):
27 """ A PygmentsHighlighter that understands and ignores prompts.
27 """ A PygmentsHighlighter that understands and ignores prompts.
28 """
28 """
29
29
30 def __init__(self, frontend):
30 def __init__(self, frontend):
31 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 self._current_offset = 0
32 self._current_offset = 0
33 self._frontend = frontend
33 self._frontend = frontend
34 self.highlighting_on = False
34 self.highlighting_on = False
35
35
36 def highlightBlock(self, string):
36 def highlightBlock(self, string):
37 """ Highlight a block of text. Reimplemented to highlight selectively.
37 """ Highlight a block of text. Reimplemented to highlight selectively.
38 """
38 """
39 if not self.highlighting_on:
39 if not self.highlighting_on:
40 return
40 return
41
41
42 # The input to this function is a unicode string that may contain
42 # The input to this function is a unicode string that may contain
43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 # the string as plain text so we can compare it.
44 # the string as plain text so we can compare it.
45 current_block = self.currentBlock()
45 current_block = self.currentBlock()
46 string = self._frontend._get_block_plain_text(current_block)
46 string = self._frontend._get_block_plain_text(current_block)
47
47
48 # Decide whether to check for the regular or continuation prompt.
48 # Decide whether to check for the regular or continuation prompt.
49 if current_block.contains(self._frontend._prompt_pos):
49 if current_block.contains(self._frontend._prompt_pos):
50 prompt = self._frontend._prompt
50 prompt = self._frontend._prompt
51 else:
51 else:
52 prompt = self._frontend._continuation_prompt
52 prompt = self._frontend._continuation_prompt
53
53
54 # Only highlight if we can identify a prompt, but make sure not to
54 # Only highlight if we can identify a prompt, but make sure not to
55 # highlight the prompt.
55 # highlight the prompt.
56 if string.startswith(prompt):
56 if string.startswith(prompt):
57 self._current_offset = len(prompt)
57 self._current_offset = len(prompt)
58 string = string[len(prompt):]
58 string = string[len(prompt):]
59 super(FrontendHighlighter, self).highlightBlock(string)
59 super(FrontendHighlighter, self).highlightBlock(string)
60
60
61 def rehighlightBlock(self, block):
61 def rehighlightBlock(self, block):
62 """ Reimplemented to temporarily enable highlighting if disabled.
62 """ Reimplemented to temporarily enable highlighting if disabled.
63 """
63 """
64 old = self.highlighting_on
64 old = self.highlighting_on
65 self.highlighting_on = True
65 self.highlighting_on = True
66 super(FrontendHighlighter, self).rehighlightBlock(block)
66 super(FrontendHighlighter, self).rehighlightBlock(block)
67 self.highlighting_on = old
67 self.highlighting_on = old
68
68
69 def setFormat(self, start, count, format):
69 def setFormat(self, start, count, format):
70 """ Reimplemented to highlight selectively.
70 """ Reimplemented to highlight selectively.
71 """
71 """
72 start += self._current_offset
72 start += self._current_offset
73 super(FrontendHighlighter, self).setFormat(start, count, format)
73 super(FrontendHighlighter, self).setFormat(start, count, format)
74
74
75
75
76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 """ A Qt frontend for a generic Python kernel.
77 """ A Qt frontend for a generic Python kernel.
78 """
78 """
79
79
80 # The text to show when the kernel is (re)started.
80 # The text to show when the kernel is (re)started.
81 banner = Unicode(config=True)
81 banner = Unicode(config=True)
82
82
83 # An option and corresponding signal for overriding the default kernel
83 # An option and corresponding signal for overriding the default kernel
84 # interrupt behavior.
84 # interrupt behavior.
85 custom_interrupt = Bool(False)
85 custom_interrupt = Bool(False)
86 custom_interrupt_requested = QtCore.Signal()
86 custom_interrupt_requested = QtCore.Signal()
87
87
88 # An option and corresponding signals for overriding the default kernel
88 # An option and corresponding signals for overriding the default kernel
89 # restart behavior.
89 # restart behavior.
90 custom_restart = Bool(False)
90 custom_restart = Bool(False)
91 custom_restart_kernel_died = QtCore.Signal(float)
91 custom_restart_kernel_died = QtCore.Signal(float)
92 custom_restart_requested = QtCore.Signal()
92 custom_restart_requested = QtCore.Signal()
93
93
94 # Whether to automatically show calltips on open-parentheses.
94 # Whether to automatically show calltips on open-parentheses.
95 enable_calltips = Bool(True, config=True,
95 enable_calltips = Bool(True, config=True,
96 help="Whether to draw information calltips on open-parentheses.")
96 help="Whether to draw information calltips on open-parentheses.")
97
97
98 clear_on_kernel_restart = Bool(True, config=True,
98 clear_on_kernel_restart = Bool(True, config=True,
99 help="Whether to clear the console when the kernel is restarted")
99 help="Whether to clear the console when the kernel is restarted")
100
100
101 confirm_restart = Bool(True, config=True,
101 confirm_restart = Bool(True, config=True,
102 help="Whether to ask for user confirmation when restarting kernel")
102 help="Whether to ask for user confirmation when restarting kernel")
103
103
104 # Emitted when a user visible 'execute_request' has been submitted to the
104 # Emitted when a user visible 'execute_request' has been submitted to the
105 # kernel from the FrontendWidget. Contains the code to be executed.
105 # kernel from the FrontendWidget. Contains the code to be executed.
106 executing = QtCore.Signal(object)
106 executing = QtCore.Signal(object)
107
107
108 # Emitted when a user-visible 'execute_reply' has been received from the
108 # Emitted when a user-visible 'execute_reply' has been received from the
109 # kernel and processed by the FrontendWidget. Contains the response message.
109 # kernel and processed by the FrontendWidget. Contains the response message.
110 executed = QtCore.Signal(object)
110 executed = QtCore.Signal(object)
111
111
112 # Emitted when an exit request has been received from the kernel.
112 # Emitted when an exit request has been received from the kernel.
113 exit_requested = QtCore.Signal(object)
113 exit_requested = QtCore.Signal(object)
114
114
115 # Protected class variables.
115 # Protected class variables.
116 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
116 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
117 logical_line_transforms=[],
117 logical_line_transforms=[],
118 python_line_transforms=[],
118 python_line_transforms=[],
119 )
119 )
120 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
120 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
121 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
121 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
122 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
122 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
123 _input_splitter_class = InputSplitter
123 _input_splitter_class = InputSplitter
124 _local_kernel = False
124 _local_kernel = False
125 _highlighter = Instance(FrontendHighlighter)
125 _highlighter = Instance(FrontendHighlighter)
126
126
127 #---------------------------------------------------------------------------
127 #---------------------------------------------------------------------------
128 # 'object' interface
128 # 'object' interface
129 #---------------------------------------------------------------------------
129 #---------------------------------------------------------------------------
130
130
131 def __init__(self, *args, **kw):
131 def __init__(self, *args, **kw):
132 super(FrontendWidget, self).__init__(*args, **kw)
132 super(FrontendWidget, self).__init__(*args, **kw)
133 # FIXME: remove this when PySide min version is updated past 1.0.7
133 # FIXME: remove this when PySide min version is updated past 1.0.7
134 # forcefully disable calltips if PySide is < 1.0.7, because they crash
134 # forcefully disable calltips if PySide is < 1.0.7, because they crash
135 if qt.QT_API == qt.QT_API_PYSIDE:
135 if qt.QT_API == qt.QT_API_PYSIDE:
136 import PySide
136 import PySide
137 if PySide.__version_info__ < (1,0,7):
137 if PySide.__version_info__ < (1,0,7):
138 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
138 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
139 self.enable_calltips = False
139 self.enable_calltips = False
140
140
141 # FrontendWidget protected variables.
141 # FrontendWidget protected variables.
142 self._bracket_matcher = BracketMatcher(self._control)
142 self._bracket_matcher = BracketMatcher(self._control)
143 self._call_tip_widget = CallTipWidget(self._control)
143 self._call_tip_widget = CallTipWidget(self._control)
144 self._completion_lexer = CompletionLexer(PythonLexer())
144 self._completion_lexer = CompletionLexer(PythonLexer())
145 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
145 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
146 self._hidden = False
146 self._hidden = False
147 self._highlighter = FrontendHighlighter(self)
147 self._highlighter = FrontendHighlighter(self)
148 self._input_splitter = self._input_splitter_class()
148 self._input_splitter = self._input_splitter_class()
149 self._kernel_manager = None
149 self._kernel_manager = None
150 self._kernel_client = None
150 self._kernel_client = None
151 self._request_info = {}
151 self._request_info = {}
152 self._request_info['execute'] = {};
152 self._request_info['execute'] = {};
153 self._callback_dict = {}
153 self._callback_dict = {}
154
154
155 # Configure the ConsoleWidget.
155 # Configure the ConsoleWidget.
156 self.tab_width = 4
156 self.tab_width = 4
157 self._set_continuation_prompt('... ')
157 self._set_continuation_prompt('... ')
158
158
159 # Configure the CallTipWidget.
159 # Configure the CallTipWidget.
160 self._call_tip_widget.setFont(self.font)
160 self._call_tip_widget.setFont(self.font)
161 self.font_changed.connect(self._call_tip_widget.setFont)
161 self.font_changed.connect(self._call_tip_widget.setFont)
162
162
163 # Configure actions.
163 # Configure actions.
164 action = self._copy_raw_action
164 action = self._copy_raw_action
165 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
165 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
166 action.setEnabled(False)
166 action.setEnabled(False)
167 action.setShortcut(QtGui.QKeySequence(key))
167 action.setShortcut(QtGui.QKeySequence(key))
168 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
168 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
169 action.triggered.connect(self.copy_raw)
169 action.triggered.connect(self.copy_raw)
170 self.copy_available.connect(action.setEnabled)
170 self.copy_available.connect(action.setEnabled)
171 self.addAction(action)
171 self.addAction(action)
172
172
173 # Connect signal handlers.
173 # Connect signal handlers.
174 document = self._control.document()
174 document = self._control.document()
175 document.contentsChange.connect(self._document_contents_change)
175 document.contentsChange.connect(self._document_contents_change)
176
176
177 # Set flag for whether we are connected via localhost.
177 # Set flag for whether we are connected via localhost.
178 self._local_kernel = kw.get('local_kernel',
178 self._local_kernel = kw.get('local_kernel',
179 FrontendWidget._local_kernel)
179 FrontendWidget._local_kernel)
180
180
181 #---------------------------------------------------------------------------
181 #---------------------------------------------------------------------------
182 # 'ConsoleWidget' public interface
182 # 'ConsoleWidget' public interface
183 #---------------------------------------------------------------------------
183 #---------------------------------------------------------------------------
184
184
185 def copy(self):
185 def copy(self):
186 """ Copy the currently selected text to the clipboard, removing prompts.
186 """ Copy the currently selected text to the clipboard, removing prompts.
187 """
187 """
188 if self._page_control is not None and self._page_control.hasFocus():
188 if self._page_control is not None and self._page_control.hasFocus():
189 self._page_control.copy()
189 self._page_control.copy()
190 elif self._control.hasFocus():
190 elif self._control.hasFocus():
191 text = self._control.textCursor().selection().toPlainText()
191 text = self._control.textCursor().selection().toPlainText()
192 if text:
192 if text:
193 text = self._prompt_transformer.transform_cell(text)
193 text = self._prompt_transformer.transform_cell(text)
194 QtGui.QApplication.clipboard().setText(text)
194 QtGui.QApplication.clipboard().setText(text)
195 else:
195 else:
196 self.log.debug("frontend widget : unknown copy target")
196 self.log.debug("frontend widget : unknown copy target")
197
197
198 #---------------------------------------------------------------------------
198 #---------------------------------------------------------------------------
199 # 'ConsoleWidget' abstract interface
199 # 'ConsoleWidget' abstract interface
200 #---------------------------------------------------------------------------
200 #---------------------------------------------------------------------------
201
201
202 def _is_complete(self, source, interactive):
202 def _is_complete(self, source, interactive):
203 """ Returns whether 'source' can be completely processed and a new
203 """ Returns whether 'source' can be completely processed and a new
204 prompt created. When triggered by an Enter/Return key press,
204 prompt created. When triggered by an Enter/Return key press,
205 'interactive' is True; otherwise, it is False.
205 'interactive' is True; otherwise, it is False.
206 """
206 """
207 self._input_splitter.reset()
207 try:
208 complete = self._input_splitter.push(source)
208 self._input_splitter.reset()
209 except SyntaxError:
210 pass
211 try:
212 complete = self._input_splitter.push(source)
213 except SyntaxError:
214 return True
209 if interactive:
215 if interactive:
210 complete = not self._input_splitter.push_accepts_more()
216 complete = not self._input_splitter.push_accepts_more()
211 return complete
217 return complete
212
218
213 def _execute(self, source, hidden):
219 def _execute(self, source, hidden):
214 """ Execute 'source'. If 'hidden', do not show any output.
220 """ Execute 'source'. If 'hidden', do not show any output.
215
221
216 See parent class :meth:`execute` docstring for full details.
222 See parent class :meth:`execute` docstring for full details.
217 """
223 """
218 msg_id = self.kernel_client.execute(source, hidden)
224 msg_id = self.kernel_client.execute(source, hidden)
219 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
225 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
220 self._hidden = hidden
226 self._hidden = hidden
221 if not hidden:
227 if not hidden:
222 self.executing.emit(source)
228 self.executing.emit(source)
223
229
224 def _prompt_started_hook(self):
230 def _prompt_started_hook(self):
225 """ Called immediately after a new prompt is displayed.
231 """ Called immediately after a new prompt is displayed.
226 """
232 """
227 if not self._reading:
233 if not self._reading:
228 self._highlighter.highlighting_on = True
234 self._highlighter.highlighting_on = True
229
235
230 def _prompt_finished_hook(self):
236 def _prompt_finished_hook(self):
231 """ Called immediately after a prompt is finished, i.e. when some input
237 """ Called immediately after a prompt is finished, i.e. when some input
232 will be processed and a new prompt displayed.
238 will be processed and a new prompt displayed.
233 """
239 """
234 # Flush all state from the input splitter so the next round of
240 # Flush all state from the input splitter so the next round of
235 # reading input starts with a clean buffer.
241 # reading input starts with a clean buffer.
236 self._input_splitter.reset()
242 try:
243 self._input_splitter.reset()
244 except SyntaxError:
245 pass
237
246
238 if not self._reading:
247 if not self._reading:
239 self._highlighter.highlighting_on = False
248 self._highlighter.highlighting_on = False
240
249
241 def _tab_pressed(self):
250 def _tab_pressed(self):
242 """ Called when the tab key is pressed. Returns whether to continue
251 """ Called when the tab key is pressed. Returns whether to continue
243 processing the event.
252 processing the event.
244 """
253 """
245 # Perform tab completion if:
254 # Perform tab completion if:
246 # 1) The cursor is in the input buffer.
255 # 1) The cursor is in the input buffer.
247 # 2) There is a non-whitespace character before the cursor.
256 # 2) There is a non-whitespace character before the cursor.
248 text = self._get_input_buffer_cursor_line()
257 text = self._get_input_buffer_cursor_line()
249 if text is None:
258 if text is None:
250 return False
259 return False
251 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
260 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
252 if complete:
261 if complete:
253 self._complete()
262 self._complete()
254 return not complete
263 return not complete
255
264
256 #---------------------------------------------------------------------------
265 #---------------------------------------------------------------------------
257 # 'ConsoleWidget' protected interface
266 # 'ConsoleWidget' protected interface
258 #---------------------------------------------------------------------------
267 #---------------------------------------------------------------------------
259
268
260 def _context_menu_make(self, pos):
269 def _context_menu_make(self, pos):
261 """ Reimplemented to add an action for raw copy.
270 """ Reimplemented to add an action for raw copy.
262 """
271 """
263 menu = super(FrontendWidget, self)._context_menu_make(pos)
272 menu = super(FrontendWidget, self)._context_menu_make(pos)
264 for before_action in menu.actions():
273 for before_action in menu.actions():
265 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
274 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
266 QtGui.QKeySequence.ExactMatch:
275 QtGui.QKeySequence.ExactMatch:
267 menu.insertAction(before_action, self._copy_raw_action)
276 menu.insertAction(before_action, self._copy_raw_action)
268 break
277 break
269 return menu
278 return menu
270
279
271 def request_interrupt_kernel(self):
280 def request_interrupt_kernel(self):
272 if self._executing:
281 if self._executing:
273 self.interrupt_kernel()
282 self.interrupt_kernel()
274
283
275 def request_restart_kernel(self):
284 def request_restart_kernel(self):
276 message = 'Are you sure you want to restart the kernel?'
285 message = 'Are you sure you want to restart the kernel?'
277 self.restart_kernel(message, now=False)
286 self.restart_kernel(message, now=False)
278
287
279 def _event_filter_console_keypress(self, event):
288 def _event_filter_console_keypress(self, event):
280 """ Reimplemented for execution interruption and smart backspace.
289 """ Reimplemented for execution interruption and smart backspace.
281 """
290 """
282 key = event.key()
291 key = event.key()
283 if self._control_key_down(event.modifiers(), include_command=False):
292 if self._control_key_down(event.modifiers(), include_command=False):
284
293
285 if key == QtCore.Qt.Key_C and self._executing:
294 if key == QtCore.Qt.Key_C and self._executing:
286 self.request_interrupt_kernel()
295 self.request_interrupt_kernel()
287 return True
296 return True
288
297
289 elif key == QtCore.Qt.Key_Period:
298 elif key == QtCore.Qt.Key_Period:
290 self.request_restart_kernel()
299 self.request_restart_kernel()
291 return True
300 return True
292
301
293 elif not event.modifiers() & QtCore.Qt.AltModifier:
302 elif not event.modifiers() & QtCore.Qt.AltModifier:
294
303
295 # Smart backspace: remove four characters in one backspace if:
304 # Smart backspace: remove four characters in one backspace if:
296 # 1) everything left of the cursor is whitespace
305 # 1) everything left of the cursor is whitespace
297 # 2) the four characters immediately left of the cursor are spaces
306 # 2) the four characters immediately left of the cursor are spaces
298 if key == QtCore.Qt.Key_Backspace:
307 if key == QtCore.Qt.Key_Backspace:
299 col = self._get_input_buffer_cursor_column()
308 col = self._get_input_buffer_cursor_column()
300 cursor = self._control.textCursor()
309 cursor = self._control.textCursor()
301 if col > 3 and not cursor.hasSelection():
310 if col > 3 and not cursor.hasSelection():
302 text = self._get_input_buffer_cursor_line()[:col]
311 text = self._get_input_buffer_cursor_line()[:col]
303 if text.endswith(' ') and not text.strip():
312 if text.endswith(' ') and not text.strip():
304 cursor.movePosition(QtGui.QTextCursor.Left,
313 cursor.movePosition(QtGui.QTextCursor.Left,
305 QtGui.QTextCursor.KeepAnchor, 4)
314 QtGui.QTextCursor.KeepAnchor, 4)
306 cursor.removeSelectedText()
315 cursor.removeSelectedText()
307 return True
316 return True
308
317
309 return super(FrontendWidget, self)._event_filter_console_keypress(event)
318 return super(FrontendWidget, self)._event_filter_console_keypress(event)
310
319
311 def _insert_continuation_prompt(self, cursor):
320 def _insert_continuation_prompt(self, cursor):
312 """ Reimplemented for auto-indentation.
321 """ Reimplemented for auto-indentation.
313 """
322 """
314 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
323 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
315 cursor.insertText(' ' * self._input_splitter.indent_spaces)
324 cursor.insertText(' ' * self._input_splitter.indent_spaces)
316
325
317 #---------------------------------------------------------------------------
326 #---------------------------------------------------------------------------
318 # 'BaseFrontendMixin' abstract interface
327 # 'BaseFrontendMixin' abstract interface
319 #---------------------------------------------------------------------------
328 #---------------------------------------------------------------------------
320
329
321 def _handle_complete_reply(self, rep):
330 def _handle_complete_reply(self, rep):
322 """ Handle replies for tab completion.
331 """ Handle replies for tab completion.
323 """
332 """
324 self.log.debug("complete: %s", rep.get('content', ''))
333 self.log.debug("complete: %s", rep.get('content', ''))
325 cursor = self._get_cursor()
334 cursor = self._get_cursor()
326 info = self._request_info.get('complete')
335 info = self._request_info.get('complete')
327 if info and info.id == rep['parent_header']['msg_id'] and \
336 if info and info.id == rep['parent_header']['msg_id'] and \
328 info.pos == cursor.position():
337 info.pos == cursor.position():
329 text = '.'.join(self._get_context())
338 text = '.'.join(self._get_context())
330 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
339 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
331 self._complete_with_items(cursor, rep['content']['matches'])
340 self._complete_with_items(cursor, rep['content']['matches'])
332
341
333 def _silent_exec_callback(self, expr, callback):
342 def _silent_exec_callback(self, expr, callback):
334 """Silently execute `expr` in the kernel and call `callback` with reply
343 """Silently execute `expr` in the kernel and call `callback` with reply
335
344
336 the `expr` is evaluated silently in the kernel (without) output in
345 the `expr` is evaluated silently in the kernel (without) output in
337 the frontend. Call `callback` with the
346 the frontend. Call `callback` with the
338 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
347 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
339
348
340 Parameters
349 Parameters
341 ----------
350 ----------
342 expr : string
351 expr : string
343 valid string to be executed by the kernel.
352 valid string to be executed by the kernel.
344 callback : function
353 callback : function
345 function accepting one argument, as a string. The string will be
354 function accepting one argument, as a string. The string will be
346 the `repr` of the result of evaluating `expr`
355 the `repr` of the result of evaluating `expr`
347
356
348 The `callback` is called with the `repr()` of the result of `expr` as
357 The `callback` is called with the `repr()` of the result of `expr` as
349 first argument. To get the object, do `eval()` on the passed value.
358 first argument. To get the object, do `eval()` on the passed value.
350
359
351 See Also
360 See Also
352 --------
361 --------
353 _handle_exec_callback : private method, deal with calling callback with reply
362 _handle_exec_callback : private method, deal with calling callback with reply
354
363
355 """
364 """
356
365
357 # generate uuid, which would be used as an indication of whether or
366 # generate uuid, which would be used as an indication of whether or
358 # not the unique request originated from here (can use msg id ?)
367 # not the unique request originated from here (can use msg id ?)
359 local_uuid = str(uuid.uuid1())
368 local_uuid = str(uuid.uuid1())
360 msg_id = self.kernel_client.execute('',
369 msg_id = self.kernel_client.execute('',
361 silent=True, user_expressions={ local_uuid:expr })
370 silent=True, user_expressions={ local_uuid:expr })
362 self._callback_dict[local_uuid] = callback
371 self._callback_dict[local_uuid] = callback
363 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
372 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
364
373
365 def _handle_exec_callback(self, msg):
374 def _handle_exec_callback(self, msg):
366 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
375 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
367
376
368 Parameters
377 Parameters
369 ----------
378 ----------
370 msg : raw message send by the kernel containing an `user_expressions`
379 msg : raw message send by the kernel containing an `user_expressions`
371 and having a 'silent_exec_callback' kind.
380 and having a 'silent_exec_callback' kind.
372
381
373 Notes
382 Notes
374 -----
383 -----
375 This function will look for a `callback` associated with the
384 This function will look for a `callback` associated with the
376 corresponding message id. Association has been made by
385 corresponding message id. Association has been made by
377 `_silent_exec_callback`. `callback` is then called with the `repr()`
386 `_silent_exec_callback`. `callback` is then called with the `repr()`
378 of the value of corresponding `user_expressions` as argument.
387 of the value of corresponding `user_expressions` as argument.
379 `callback` is then removed from the known list so that any message
388 `callback` is then removed from the known list so that any message
380 coming again with the same id won't trigger it.
389 coming again with the same id won't trigger it.
381
390
382 """
391 """
383
392
384 user_exp = msg['content'].get('user_expressions')
393 user_exp = msg['content'].get('user_expressions')
385 if not user_exp:
394 if not user_exp:
386 return
395 return
387 for expression in user_exp:
396 for expression in user_exp:
388 if expression in self._callback_dict:
397 if expression in self._callback_dict:
389 self._callback_dict.pop(expression)(user_exp[expression])
398 self._callback_dict.pop(expression)(user_exp[expression])
390
399
391 def _handle_execute_reply(self, msg):
400 def _handle_execute_reply(self, msg):
392 """ Handles replies for code execution.
401 """ Handles replies for code execution.
393 """
402 """
394 self.log.debug("execute: %s", msg.get('content', ''))
403 self.log.debug("execute: %s", msg.get('content', ''))
395 msg_id = msg['parent_header']['msg_id']
404 msg_id = msg['parent_header']['msg_id']
396 info = self._request_info['execute'].get(msg_id)
405 info = self._request_info['execute'].get(msg_id)
397 # unset reading flag, because if execute finished, raw_input can't
406 # unset reading flag, because if execute finished, raw_input can't
398 # still be pending.
407 # still be pending.
399 self._reading = False
408 self._reading = False
400 if info and info.kind == 'user' and not self._hidden:
409 if info and info.kind == 'user' and not self._hidden:
401 # Make sure that all output from the SUB channel has been processed
410 # Make sure that all output from the SUB channel has been processed
402 # before writing a new prompt.
411 # before writing a new prompt.
403 self.kernel_client.iopub_channel.flush()
412 self.kernel_client.iopub_channel.flush()
404
413
405 # Reset the ANSI style information to prevent bad text in stdout
414 # Reset the ANSI style information to prevent bad text in stdout
406 # from messing up our colors. We're not a true terminal so we're
415 # from messing up our colors. We're not a true terminal so we're
407 # allowed to do this.
416 # allowed to do this.
408 if self.ansi_codes:
417 if self.ansi_codes:
409 self._ansi_processor.reset_sgr()
418 self._ansi_processor.reset_sgr()
410
419
411 content = msg['content']
420 content = msg['content']
412 status = content['status']
421 status = content['status']
413 if status == 'ok':
422 if status == 'ok':
414 self._process_execute_ok(msg)
423 self._process_execute_ok(msg)
415 elif status == 'error':
424 elif status == 'error':
416 self._process_execute_error(msg)
425 self._process_execute_error(msg)
417 elif status == 'aborted':
426 elif status == 'aborted':
418 self._process_execute_abort(msg)
427 self._process_execute_abort(msg)
419
428
420 self._show_interpreter_prompt_for_reply(msg)
429 self._show_interpreter_prompt_for_reply(msg)
421 self.executed.emit(msg)
430 self.executed.emit(msg)
422 self._request_info['execute'].pop(msg_id)
431 self._request_info['execute'].pop(msg_id)
423 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
432 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
424 self._handle_exec_callback(msg)
433 self._handle_exec_callback(msg)
425 self._request_info['execute'].pop(msg_id)
434 self._request_info['execute'].pop(msg_id)
426 else:
435 else:
427 super(FrontendWidget, self)._handle_execute_reply(msg)
436 super(FrontendWidget, self)._handle_execute_reply(msg)
428
437
429 def _handle_input_request(self, msg):
438 def _handle_input_request(self, msg):
430 """ Handle requests for raw_input.
439 """ Handle requests for raw_input.
431 """
440 """
432 self.log.debug("input: %s", msg.get('content', ''))
441 self.log.debug("input: %s", msg.get('content', ''))
433 if self._hidden:
442 if self._hidden:
434 raise RuntimeError('Request for raw input during hidden execution.')
443 raise RuntimeError('Request for raw input during hidden execution.')
435
444
436 # Make sure that all output from the SUB channel has been processed
445 # Make sure that all output from the SUB channel has been processed
437 # before entering readline mode.
446 # before entering readline mode.
438 self.kernel_client.iopub_channel.flush()
447 self.kernel_client.iopub_channel.flush()
439
448
440 def callback(line):
449 def callback(line):
441 self.kernel_client.stdin_channel.input(line)
450 self.kernel_client.stdin_channel.input(line)
442 if self._reading:
451 if self._reading:
443 self.log.debug("Got second input request, assuming first was interrupted.")
452 self.log.debug("Got second input request, assuming first was interrupted.")
444 self._reading = False
453 self._reading = False
445 self._readline(msg['content']['prompt'], callback=callback)
454 self._readline(msg['content']['prompt'], callback=callback)
446
455
447 def _kernel_restarted_message(self, died=True):
456 def _kernel_restarted_message(self, died=True):
448 msg = "Kernel died, restarting" if died else "Kernel restarting"
457 msg = "Kernel died, restarting" if died else "Kernel restarting"
449 self._append_html("<br>%s<hr><br>" % msg,
458 self._append_html("<br>%s<hr><br>" % msg,
450 before_prompt=False
459 before_prompt=False
451 )
460 )
452
461
453 def _handle_kernel_died(self, since_last_heartbeat):
462 def _handle_kernel_died(self, since_last_heartbeat):
454 """Handle the kernel's death (if we do not own the kernel).
463 """Handle the kernel's death (if we do not own the kernel).
455 """
464 """
456 self.log.warn("kernel died: %s", since_last_heartbeat)
465 self.log.warn("kernel died: %s", since_last_heartbeat)
457 if self.custom_restart:
466 if self.custom_restart:
458 self.custom_restart_kernel_died.emit(since_last_heartbeat)
467 self.custom_restart_kernel_died.emit(since_last_heartbeat)
459 else:
468 else:
460 self._kernel_restarted_message(died=True)
469 self._kernel_restarted_message(died=True)
461 self.reset()
470 self.reset()
462
471
463 def _handle_kernel_restarted(self, died=True):
472 def _handle_kernel_restarted(self, died=True):
464 """Notice that the autorestarter restarted the kernel.
473 """Notice that the autorestarter restarted the kernel.
465
474
466 There's nothing to do but show a message.
475 There's nothing to do but show a message.
467 """
476 """
468 self.log.warn("kernel restarted")
477 self.log.warn("kernel restarted")
469 self._kernel_restarted_message(died=died)
478 self._kernel_restarted_message(died=died)
470 self.reset()
479 self.reset()
471
480
472 def _handle_object_info_reply(self, rep):
481 def _handle_object_info_reply(self, rep):
473 """ Handle replies for call tips.
482 """ Handle replies for call tips.
474 """
483 """
475 self.log.debug("oinfo: %s", rep.get('content', ''))
484 self.log.debug("oinfo: %s", rep.get('content', ''))
476 cursor = self._get_cursor()
485 cursor = self._get_cursor()
477 info = self._request_info.get('call_tip')
486 info = self._request_info.get('call_tip')
478 if info and info.id == rep['parent_header']['msg_id'] and \
487 if info and info.id == rep['parent_header']['msg_id'] and \
479 info.pos == cursor.position():
488 info.pos == cursor.position():
480 # Get the information for a call tip. For now we format the call
489 # Get the information for a call tip. For now we format the call
481 # line as string, later we can pass False to format_call and
490 # line as string, later we can pass False to format_call and
482 # syntax-highlight it ourselves for nicer formatting in the
491 # syntax-highlight it ourselves for nicer formatting in the
483 # calltip.
492 # calltip.
484 content = rep['content']
493 content = rep['content']
485 # if this is from pykernel, 'docstring' will be the only key
494 # if this is from pykernel, 'docstring' will be the only key
486 if content.get('ismagic', False):
495 if content.get('ismagic', False):
487 # Don't generate a call-tip for magics. Ideally, we should
496 # Don't generate a call-tip for magics. Ideally, we should
488 # generate a tooltip, but not on ( like we do for actual
497 # generate a tooltip, but not on ( like we do for actual
489 # callables.
498 # callables.
490 call_info, doc = None, None
499 call_info, doc = None, None
491 else:
500 else:
492 call_info, doc = call_tip(content, format_call=True)
501 call_info, doc = call_tip(content, format_call=True)
493 if call_info or doc:
502 if call_info or doc:
494 self._call_tip_widget.show_call_info(call_info, doc)
503 self._call_tip_widget.show_call_info(call_info, doc)
495
504
496 def _handle_pyout(self, msg):
505 def _handle_pyout(self, msg):
497 """ Handle display hook output.
506 """ Handle display hook output.
498 """
507 """
499 self.log.debug("pyout: %s", msg.get('content', ''))
508 self.log.debug("pyout: %s", msg.get('content', ''))
500 if not self._hidden and self._is_from_this_session(msg):
509 if not self._hidden and self._is_from_this_session(msg):
501 text = msg['content']['data']
510 text = msg['content']['data']
502 self._append_plain_text(text + '\n', before_prompt=True)
511 self._append_plain_text(text + '\n', before_prompt=True)
503
512
504 def _handle_stream(self, msg):
513 def _handle_stream(self, msg):
505 """ Handle stdout, stderr, and stdin.
514 """ Handle stdout, stderr, and stdin.
506 """
515 """
507 self.log.debug("stream: %s", msg.get('content', ''))
516 self.log.debug("stream: %s", msg.get('content', ''))
508 if not self._hidden and self._is_from_this_session(msg):
517 if not self._hidden and self._is_from_this_session(msg):
509 # Most consoles treat tabs as being 8 space characters. Convert tabs
518 # Most consoles treat tabs as being 8 space characters. Convert tabs
510 # to spaces so that output looks as expected regardless of this
519 # to spaces so that output looks as expected regardless of this
511 # widget's tab width.
520 # widget's tab width.
512 text = msg['content']['data'].expandtabs(8)
521 text = msg['content']['data'].expandtabs(8)
513
522
514 self._append_plain_text(text, before_prompt=True)
523 self._append_plain_text(text, before_prompt=True)
515 self._control.moveCursor(QtGui.QTextCursor.End)
524 self._control.moveCursor(QtGui.QTextCursor.End)
516
525
517 def _handle_shutdown_reply(self, msg):
526 def _handle_shutdown_reply(self, msg):
518 """ Handle shutdown signal, only if from other console.
527 """ Handle shutdown signal, only if from other console.
519 """
528 """
520 self.log.warn("shutdown: %s", msg.get('content', ''))
529 self.log.warn("shutdown: %s", msg.get('content', ''))
521 restart = msg.get('content', {}).get('restart', False)
530 restart = msg.get('content', {}).get('restart', False)
522 if not self._hidden and not self._is_from_this_session(msg):
531 if not self._hidden and not self._is_from_this_session(msg):
523 # got shutdown reply, request came from session other than ours
532 # got shutdown reply, request came from session other than ours
524 if restart:
533 if restart:
525 # someone restarted the kernel, handle it
534 # someone restarted the kernel, handle it
526 self._handle_kernel_restarted(died=False)
535 self._handle_kernel_restarted(died=False)
527 else:
536 else:
528 # kernel was shutdown permanently
537 # kernel was shutdown permanently
529 # this triggers exit_requested if the kernel was local,
538 # this triggers exit_requested if the kernel was local,
530 # and a dialog if the kernel was remote,
539 # and a dialog if the kernel was remote,
531 # so we don't suddenly clear the qtconsole without asking.
540 # so we don't suddenly clear the qtconsole without asking.
532 if self._local_kernel:
541 if self._local_kernel:
533 self.exit_requested.emit(self)
542 self.exit_requested.emit(self)
534 else:
543 else:
535 title = self.window().windowTitle()
544 title = self.window().windowTitle()
536 reply = QtGui.QMessageBox.question(self, title,
545 reply = QtGui.QMessageBox.question(self, title,
537 "Kernel has been shutdown permanently. "
546 "Kernel has been shutdown permanently. "
538 "Close the Console?",
547 "Close the Console?",
539 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
548 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
540 if reply == QtGui.QMessageBox.Yes:
549 if reply == QtGui.QMessageBox.Yes:
541 self.exit_requested.emit(self)
550 self.exit_requested.emit(self)
542
551
543 def _handle_status(self, msg):
552 def _handle_status(self, msg):
544 """Handle status message"""
553 """Handle status message"""
545 # This is where a busy/idle indicator would be triggered,
554 # This is where a busy/idle indicator would be triggered,
546 # when we make one.
555 # when we make one.
547 state = msg['content'].get('execution_state', '')
556 state = msg['content'].get('execution_state', '')
548 if state == 'starting':
557 if state == 'starting':
549 # kernel started while we were running
558 # kernel started while we were running
550 if self._executing:
559 if self._executing:
551 self._handle_kernel_restarted(died=True)
560 self._handle_kernel_restarted(died=True)
552 elif state == 'idle':
561 elif state == 'idle':
553 pass
562 pass
554 elif state == 'busy':
563 elif state == 'busy':
555 pass
564 pass
556
565
557 def _started_channels(self):
566 def _started_channels(self):
558 """ Called when the KernelManager channels have started listening or
567 """ Called when the KernelManager channels have started listening or
559 when the frontend is assigned an already listening KernelManager.
568 when the frontend is assigned an already listening KernelManager.
560 """
569 """
561 self.reset(clear=True)
570 self.reset(clear=True)
562
571
563 #---------------------------------------------------------------------------
572 #---------------------------------------------------------------------------
564 # 'FrontendWidget' public interface
573 # 'FrontendWidget' public interface
565 #---------------------------------------------------------------------------
574 #---------------------------------------------------------------------------
566
575
567 def copy_raw(self):
576 def copy_raw(self):
568 """ Copy the currently selected text to the clipboard without attempting
577 """ Copy the currently selected text to the clipboard without attempting
569 to remove prompts or otherwise alter the text.
578 to remove prompts or otherwise alter the text.
570 """
579 """
571 self._control.copy()
580 self._control.copy()
572
581
573 def execute_file(self, path, hidden=False):
582 def execute_file(self, path, hidden=False):
574 """ Attempts to execute file with 'path'. If 'hidden', no output is
583 """ Attempts to execute file with 'path'. If 'hidden', no output is
575 shown.
584 shown.
576 """
585 """
577 self.execute('execfile(%r)' % path, hidden=hidden)
586 self.execute('execfile(%r)' % path, hidden=hidden)
578
587
579 def interrupt_kernel(self):
588 def interrupt_kernel(self):
580 """ Attempts to interrupt the running kernel.
589 """ Attempts to interrupt the running kernel.
581
590
582 Also unsets _reading flag, to avoid runtime errors
591 Also unsets _reading flag, to avoid runtime errors
583 if raw_input is called again.
592 if raw_input is called again.
584 """
593 """
585 if self.custom_interrupt:
594 if self.custom_interrupt:
586 self._reading = False
595 self._reading = False
587 self.custom_interrupt_requested.emit()
596 self.custom_interrupt_requested.emit()
588 elif self.kernel_manager:
597 elif self.kernel_manager:
589 self._reading = False
598 self._reading = False
590 self.kernel_manager.interrupt_kernel()
599 self.kernel_manager.interrupt_kernel()
591 else:
600 else:
592 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
601 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
593
602
594 def reset(self, clear=False):
603 def reset(self, clear=False):
595 """ Resets the widget to its initial state if ``clear`` parameter
604 """ Resets the widget to its initial state if ``clear`` parameter
596 is True, otherwise
605 is True, otherwise
597 prints a visual indication of the fact that the kernel restarted, but
606 prints a visual indication of the fact that the kernel restarted, but
598 does not clear the traces from previous usage of the kernel before it
607 does not clear the traces from previous usage of the kernel before it
599 was restarted. With ``clear=True``, it is similar to ``%clear``, but
608 was restarted. With ``clear=True``, it is similar to ``%clear``, but
600 also re-writes the banner and aborts execution if necessary.
609 also re-writes the banner and aborts execution if necessary.
601 """
610 """
602 if self._executing:
611 if self._executing:
603 self._executing = False
612 self._executing = False
604 self._request_info['execute'] = {}
613 self._request_info['execute'] = {}
605 self._reading = False
614 self._reading = False
606 self._highlighter.highlighting_on = False
615 self._highlighter.highlighting_on = False
607
616
608 if clear:
617 if clear:
609 self._control.clear()
618 self._control.clear()
610 self._append_plain_text(self.banner)
619 self._append_plain_text(self.banner)
611 # update output marker for stdout/stderr, so that startup
620 # update output marker for stdout/stderr, so that startup
612 # messages appear after banner:
621 # messages appear after banner:
613 self._append_before_prompt_pos = self._get_cursor().position()
622 self._append_before_prompt_pos = self._get_cursor().position()
614 self._show_interpreter_prompt()
623 self._show_interpreter_prompt()
615
624
616 def restart_kernel(self, message, now=False):
625 def restart_kernel(self, message, now=False):
617 """ Attempts to restart the running kernel.
626 """ Attempts to restart the running kernel.
618 """
627 """
619 # FIXME: now should be configurable via a checkbox in the dialog. Right
628 # FIXME: now should be configurable via a checkbox in the dialog. Right
620 # now at least the heartbeat path sets it to True and the manual restart
629 # now at least the heartbeat path sets it to True and the manual restart
621 # to False. But those should just be the pre-selected states of a
630 # to False. But those should just be the pre-selected states of a
622 # checkbox that the user could override if so desired. But I don't know
631 # checkbox that the user could override if so desired. But I don't know
623 # enough Qt to go implementing the checkbox now.
632 # enough Qt to go implementing the checkbox now.
624
633
625 if self.custom_restart:
634 if self.custom_restart:
626 self.custom_restart_requested.emit()
635 self.custom_restart_requested.emit()
627 return
636 return
628
637
629 if self.kernel_manager:
638 if self.kernel_manager:
630 # Pause the heart beat channel to prevent further warnings.
639 # Pause the heart beat channel to prevent further warnings.
631 self.kernel_client.hb_channel.pause()
640 self.kernel_client.hb_channel.pause()
632
641
633 # Prompt the user to restart the kernel. Un-pause the heartbeat if
642 # Prompt the user to restart the kernel. Un-pause the heartbeat if
634 # they decline. (If they accept, the heartbeat will be un-paused
643 # they decline. (If they accept, the heartbeat will be un-paused
635 # automatically when the kernel is restarted.)
644 # automatically when the kernel is restarted.)
636 if self.confirm_restart:
645 if self.confirm_restart:
637 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
646 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
638 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
647 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
639 message, buttons)
648 message, buttons)
640 do_restart = result == QtGui.QMessageBox.Yes
649 do_restart = result == QtGui.QMessageBox.Yes
641 else:
650 else:
642 # confirm_restart is False, so we don't need to ask user
651 # confirm_restart is False, so we don't need to ask user
643 # anything, just do the restart
652 # anything, just do the restart
644 do_restart = True
653 do_restart = True
645 if do_restart:
654 if do_restart:
646 try:
655 try:
647 self.kernel_manager.restart_kernel(now=now)
656 self.kernel_manager.restart_kernel(now=now)
648 except RuntimeError as e:
657 except RuntimeError as e:
649 self._append_plain_text(
658 self._append_plain_text(
650 'Error restarting kernel: %s\n' % e,
659 'Error restarting kernel: %s\n' % e,
651 before_prompt=True
660 before_prompt=True
652 )
661 )
653 else:
662 else:
654 self._append_html("<br>Restarting kernel...\n<hr><br>",
663 self._append_html("<br>Restarting kernel...\n<hr><br>",
655 before_prompt=True,
664 before_prompt=True,
656 )
665 )
657 else:
666 else:
658 self.kernel_client.hb_channel.unpause()
667 self.kernel_client.hb_channel.unpause()
659
668
660 else:
669 else:
661 self._append_plain_text(
670 self._append_plain_text(
662 'Cannot restart a Kernel I did not start\n',
671 'Cannot restart a Kernel I did not start\n',
663 before_prompt=True
672 before_prompt=True
664 )
673 )
665
674
666 #---------------------------------------------------------------------------
675 #---------------------------------------------------------------------------
667 # 'FrontendWidget' protected interface
676 # 'FrontendWidget' protected interface
668 #---------------------------------------------------------------------------
677 #---------------------------------------------------------------------------
669
678
670 def _call_tip(self):
679 def _call_tip(self):
671 """ Shows a call tip, if appropriate, at the current cursor location.
680 """ Shows a call tip, if appropriate, at the current cursor location.
672 """
681 """
673 # Decide if it makes sense to show a call tip
682 # Decide if it makes sense to show a call tip
674 if not self.enable_calltips:
683 if not self.enable_calltips:
675 return False
684 return False
676 cursor = self._get_cursor()
685 cursor = self._get_cursor()
677 cursor.movePosition(QtGui.QTextCursor.Left)
686 cursor.movePosition(QtGui.QTextCursor.Left)
678 if cursor.document().characterAt(cursor.position()) != '(':
687 if cursor.document().characterAt(cursor.position()) != '(':
679 return False
688 return False
680 context = self._get_context(cursor)
689 context = self._get_context(cursor)
681 if not context:
690 if not context:
682 return False
691 return False
683
692
684 # Send the metadata request to the kernel
693 # Send the metadata request to the kernel
685 name = '.'.join(context)
694 name = '.'.join(context)
686 msg_id = self.kernel_client.object_info(name)
695 msg_id = self.kernel_client.object_info(name)
687 pos = self._get_cursor().position()
696 pos = self._get_cursor().position()
688 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
697 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
689 return True
698 return True
690
699
691 def _complete(self):
700 def _complete(self):
692 """ Performs completion at the current cursor location.
701 """ Performs completion at the current cursor location.
693 """
702 """
694 context = self._get_context()
703 context = self._get_context()
695 if context:
704 if context:
696 # Send the completion request to the kernel
705 # Send the completion request to the kernel
697 msg_id = self.kernel_client.complete(
706 msg_id = self.kernel_client.complete(
698 '.'.join(context), # text
707 '.'.join(context), # text
699 self._get_input_buffer_cursor_line(), # line
708 self._get_input_buffer_cursor_line(), # line
700 self._get_input_buffer_cursor_column(), # cursor_pos
709 self._get_input_buffer_cursor_column(), # cursor_pos
701 self.input_buffer) # block
710 self.input_buffer) # block
702 pos = self._get_cursor().position()
711 pos = self._get_cursor().position()
703 info = self._CompletionRequest(msg_id, pos)
712 info = self._CompletionRequest(msg_id, pos)
704 self._request_info['complete'] = info
713 self._request_info['complete'] = info
705
714
706 def _get_context(self, cursor=None):
715 def _get_context(self, cursor=None):
707 """ Gets the context for the specified cursor (or the current cursor
716 """ Gets the context for the specified cursor (or the current cursor
708 if none is specified).
717 if none is specified).
709 """
718 """
710 if cursor is None:
719 if cursor is None:
711 cursor = self._get_cursor()
720 cursor = self._get_cursor()
712 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
721 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
713 QtGui.QTextCursor.KeepAnchor)
722 QtGui.QTextCursor.KeepAnchor)
714 text = cursor.selection().toPlainText()
723 text = cursor.selection().toPlainText()
715 return self._completion_lexer.get_context(text)
724 return self._completion_lexer.get_context(text)
716
725
717 def _process_execute_abort(self, msg):
726 def _process_execute_abort(self, msg):
718 """ Process a reply for an aborted execution request.
727 """ Process a reply for an aborted execution request.
719 """
728 """
720 self._append_plain_text("ERROR: execution aborted\n")
729 self._append_plain_text("ERROR: execution aborted\n")
721
730
722 def _process_execute_error(self, msg):
731 def _process_execute_error(self, msg):
723 """ Process a reply for an execution request that resulted in an error.
732 """ Process a reply for an execution request that resulted in an error.
724 """
733 """
725 content = msg['content']
734 content = msg['content']
726 # If a SystemExit is passed along, this means exit() was called - also
735 # If a SystemExit is passed along, this means exit() was called - also
727 # all the ipython %exit magic syntax of '-k' to be used to keep
736 # all the ipython %exit magic syntax of '-k' to be used to keep
728 # the kernel running
737 # the kernel running
729 if content['ename']=='SystemExit':
738 if content['ename']=='SystemExit':
730 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
739 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
731 self._keep_kernel_on_exit = keepkernel
740 self._keep_kernel_on_exit = keepkernel
732 self.exit_requested.emit(self)
741 self.exit_requested.emit(self)
733 else:
742 else:
734 traceback = ''.join(content['traceback'])
743 traceback = ''.join(content['traceback'])
735 self._append_plain_text(traceback)
744 self._append_plain_text(traceback)
736
745
737 def _process_execute_ok(self, msg):
746 def _process_execute_ok(self, msg):
738 """ Process a reply for a successful execution request.
747 """ Process a reply for a successful execution request.
739 """
748 """
740 payload = msg['content']['payload']
749 payload = msg['content']['payload']
741 for item in payload:
750 for item in payload:
742 if not self._process_execute_payload(item):
751 if not self._process_execute_payload(item):
743 warning = 'Warning: received unknown payload of type %s'
752 warning = 'Warning: received unknown payload of type %s'
744 print(warning % repr(item['source']))
753 print(warning % repr(item['source']))
745
754
746 def _process_execute_payload(self, item):
755 def _process_execute_payload(self, item):
747 """ Process a single payload item from the list of payload items in an
756 """ Process a single payload item from the list of payload items in an
748 execution reply. Returns whether the payload was handled.
757 execution reply. Returns whether the payload was handled.
749 """
758 """
750 # The basic FrontendWidget doesn't handle payloads, as they are a
759 # The basic FrontendWidget doesn't handle payloads, as they are a
751 # mechanism for going beyond the standard Python interpreter model.
760 # mechanism for going beyond the standard Python interpreter model.
752 return False
761 return False
753
762
754 def _show_interpreter_prompt(self):
763 def _show_interpreter_prompt(self):
755 """ Shows a prompt for the interpreter.
764 """ Shows a prompt for the interpreter.
756 """
765 """
757 self._show_prompt('>>> ')
766 self._show_prompt('>>> ')
758
767
759 def _show_interpreter_prompt_for_reply(self, msg):
768 def _show_interpreter_prompt_for_reply(self, msg):
760 """ Shows a prompt for the interpreter given an 'execute_reply' message.
769 """ Shows a prompt for the interpreter given an 'execute_reply' message.
761 """
770 """
762 self._show_interpreter_prompt()
771 self._show_interpreter_prompt()
763
772
764 #------ Signal handlers ----------------------------------------------------
773 #------ Signal handlers ----------------------------------------------------
765
774
766 def _document_contents_change(self, position, removed, added):
775 def _document_contents_change(self, position, removed, added):
767 """ Called whenever the document's content changes. Display a call tip
776 """ Called whenever the document's content changes. Display a call tip
768 if appropriate.
777 if appropriate.
769 """
778 """
770 # Calculate where the cursor should be *after* the change:
779 # Calculate where the cursor should be *after* the change:
771 position += added
780 position += added
772
781
773 document = self._control.document()
782 document = self._control.document()
774 if position == self._get_cursor().position():
783 if position == self._get_cursor().position():
775 self._call_tip()
784 self._call_tip()
776
785
777 #------ Trait default initializers -----------------------------------------
786 #------ Trait default initializers -----------------------------------------
778
787
779 def _banner_default(self):
788 def _banner_default(self):
780 """ Returns the standard Python banner.
789 """ Returns the standard Python banner.
781 """
790 """
782 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
791 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
783 '"license" for more information.'
792 '"license" for more information.'
784 return banner % (sys.version, sys.platform)
793 return banner % (sys.version, sys.platform)
General Comments 0
You need to be logged in to leave comments. Login now