linefrontendbase.py
372 lines
| 13.0 KiB
| text/x-python
|
PythonLexer
Gael Varoquaux
|
r1355 | """ | ||
gvaroquaux
|
r1455 | Base front end class for all line-oriented frontends, rather than | ||
block-oriented. | ||||
Gael Varoquaux
|
r1355 | |||
Currently this focuses on synchronous frontends. | ||||
""" | ||||
__docformat__ = "restructuredtext en" | ||||
#------------------------------------------------------------------------------- | ||||
# Copyright (C) 2008 The IPython Development Team | ||||
# | ||||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#------------------------------------------------------------------------------- | ||||
#------------------------------------------------------------------------------- | ||||
# Imports | ||||
#------------------------------------------------------------------------------- | ||||
import re | ||||
Gael Varoquaux
|
r1458 | import sys | ||
gvaroquaux
|
r1638 | import codeop | ||
Gael Varoquaux
|
r1355 | |||
from frontendbase import FrontEndBase | ||||
from IPython.kernel.core.interpreter import Interpreter | ||||
Gael Varoquaux
|
r1379 | def common_prefix(strings): | ||
gvaroquaux
|
r1455 | """ Given a list of strings, return the common prefix between all | ||
these strings. | ||||
""" | ||||
Gael Varoquaux
|
r1379 | ref = strings[0] | ||
prefix = '' | ||||
for size in range(len(ref)): | ||||
test_prefix = ref[:size+1] | ||||
for string in strings[1:]: | ||||
if not string.startswith(test_prefix): | ||||
return prefix | ||||
prefix = test_prefix | ||||
return prefix | ||||
Gael Varoquaux
|
r1355 | #------------------------------------------------------------------------------- | ||
# Base class for the line-oriented front ends | ||||
#------------------------------------------------------------------------------- | ||||
class LineFrontEndBase(FrontEndBase): | ||||
gvaroquaux
|
r1455 | """ Concrete implementation of the FrontEndBase class. This is meant | ||
to be the base class behind all the frontend that are line-oriented, | ||||
rather than block-oriented. | ||||
""" | ||||
Gael Varoquaux
|
r1355 | |||
Gael Varoquaux
|
r1360 | # We need to keep the prompt number, to be able to increment | ||
# it when there is an exception. | ||||
prompt_number = 1 | ||||
gvaroquaux
|
r1460 | # We keep a reference to the last result: it helps testing and | ||
# programatic control of the frontend. | ||||
Gael Varoquaux
|
r1373 | last_result = dict(number=0) | ||
Gael Varoquaux
|
r1360 | |||
Gael Varoquaux
|
r1893 | # The last prompt displayed. Useful for continuation prompts. | ||
last_prompt = '' | ||||
gvaroquaux
|
r1462 | # The input buffer being edited | ||
input_buffer = '' | ||||
gvaroquaux
|
r1463 | # Set to true for debug output | ||
debug = False | ||||
Gael Varoquaux
|
r1495 | # A banner to print at startup | ||
banner = None | ||||
Gael Varoquaux
|
r1355 | #-------------------------------------------------------------------------- | ||
gvaroquaux
|
r1455 | # FrontEndBase interface | ||
Gael Varoquaux
|
r1355 | #-------------------------------------------------------------------------- | ||
Gael Varoquaux
|
r1495 | def __init__(self, shell=None, history=None, banner=None, *args, **kwargs): | ||
Gael Varoquaux
|
r1355 | if shell is None: | ||
shell = Interpreter() | ||||
FrontEndBase.__init__(self, shell=shell, history=history) | ||||
Gael Varoquaux
|
r1495 | |||
if banner is not None: | ||||
self.banner = banner | ||||
Gael Varoquaux
|
r1624 | |||
def start(self): | ||||
""" Put the frontend in a state where it is ready for user | ||||
interaction. | ||||
""" | ||||
Gael Varoquaux
|
r1495 | if self.banner is not None: | ||
self.write(self.banner, refresh=False) | ||||
Gael Varoquaux
|
r1458 | self.new_prompt(self.input_prompt_template.substitute(number=1)) | ||
Gael Varoquaux
|
r1355 | |||
Gael Varoquaux
|
r1379 | def complete(self, line): | ||
"""Complete line in engine's user_ns | ||||
Gael Varoquaux
|
r1355 | |||
Parameters | ||||
---------- | ||||
Gael Varoquaux
|
r1379 | line : string | ||
Gael Varoquaux
|
r1355 | |||
Fernando Perez
|
r2109 | Returns | ||
------- | ||||
Gael Varoquaux
|
r1379 | The replacement for the line and the list of possible completions. | ||
Gael Varoquaux
|
r1355 | """ | ||
Gael Varoquaux
|
r1379 | completions = self.shell.complete(line) | ||
complete_sep = re.compile('[\s\{\}\[\]\(\)\=]') | ||||
if completions: | ||||
prefix = common_prefix(completions) | ||||
residual = complete_sep.split(line)[:-1] | ||||
line = line[:-len(residual)] + prefix | ||||
return line, completions | ||||
Gael Varoquaux
|
r1355 | |||
def render_result(self, result): | ||||
gvaroquaux
|
r1455 | """ Frontend-specific rendering of the result of a calculation | ||
that has been sent to an engine. | ||||
""" | ||||
Gael Varoquaux
|
r1355 | if 'stdout' in result and result['stdout']: | ||
self.write('\n' + result['stdout']) | ||||
if 'display' in result and result['display']: | ||||
self.write("%s%s\n" % ( | ||||
Gael Varoquaux
|
r1458 | self.output_prompt_template.substitute( | ||
number=result['number']), | ||||
Gael Varoquaux
|
r1355 | result['display']['pprint'] | ||
) ) | ||||
Gael Varoquaux
|
r1360 | |||
Gael Varoquaux
|
r1355 | def render_error(self, failure): | ||
gvaroquaux
|
r1455 | """ Frontend-specific rendering of error. | ||
""" | ||||
Gael Varoquaux
|
r1458 | self.write('\n\n'+str(failure)+'\n\n') | ||
Gael Varoquaux
|
r1355 | return failure | ||
Gael Varoquaux
|
r1360 | |||
def is_complete(self, string): | ||||
gvaroquaux
|
r1455 | """ Check if a string forms a complete, executable set of | ||
commands. | ||||
For the line-oriented frontend, multi-line code is not executed | ||||
as soon as it is complete: the users has to enter two line | ||||
returns. | ||||
""" | ||||
Gael Varoquaux
|
r1373 | if string in ('', '\n'): | ||
gvaroquaux
|
r1455 | # Prefiltering, eg through ipython0, may return an empty | ||
# string although some operations have been accomplished. We | ||||
# thus want to consider an empty string as a complete | ||||
# statement. | ||||
Gael Varoquaux
|
r1373 | return True | ||
gvaroquaux
|
r1462 | elif ( len(self.input_buffer.split('\n'))>2 | ||
Gael Varoquaux
|
r1371 | and not re.findall(r"\n[\t ]*\n[\t ]*$", string)): | ||
Gael Varoquaux
|
r1360 | return False | ||
else: | ||||
gvaroquaux
|
r1639 | self.capture_output() | ||
try: | ||||
# Add line returns here, to make sure that the statement is | ||||
Gael Varoquaux
|
r1887 | # complete (except if '\' was used). | ||
# This should probably be done in a different place (like | ||||
# maybe 'prefilter_input' method? For now, this works. | ||||
clean_string = string.rstrip('\n') | ||||
if not clean_string.endswith('\\'): clean_string +='\n\n' | ||||
is_complete = codeop.compile_command(clean_string, | ||||
gvaroquaux
|
r1639 | "<string>", "exec") | ||
self.release_output() | ||||
except Exception, e: | ||||
# XXX: Hack: return True so that the | ||||
# code gets executed and the error captured. | ||||
is_complete = True | ||||
return is_complete | ||||
Gael Varoquaux
|
r1458 | |||
gvaroquaux
|
r1479 | def write(self, string, refresh=True): | ||
Gael Varoquaux
|
r1458 | """ Write some characters to the display. | ||
Subclass should overide this method. | ||||
gvaroquaux
|
r1479 | |||
The refresh keyword argument is used in frontends with an | ||||
event loop, to choose whether the write should trigget an UI | ||||
refresh, and thus be syncrhonous, or not. | ||||
Gael Varoquaux
|
r1458 | """ | ||
print >>sys.__stderr__, string | ||||
Gael Varoquaux
|
r1360 | def execute(self, python_string, raw_string=None): | ||
gvaroquaux
|
r1455 | """ Stores the raw_string in the history, and sends the | ||
python string to the interpreter. | ||||
Gael Varoquaux
|
r1360 | """ | ||
if raw_string is None: | ||||
Gael Varoquaux
|
r1374 | raw_string = python_string | ||
Gael Varoquaux
|
r1360 | # Create a false result, in case there is an exception | ||
Gael Varoquaux
|
r1362 | self.last_result = dict(number=self.prompt_number) | ||
Fernando Perez
|
r1706 | |||
Gael Varoquaux
|
r1360 | try: | ||
Fernando Perez
|
r1706 | try: | ||
self.history.input_cache[-1] = raw_string.rstrip() | ||||
result = self.shell.execute(python_string) | ||||
self.last_result = result | ||||
self.render_result(result) | ||||
except: | ||||
self.show_traceback() | ||||
Gael Varoquaux
|
r1360 | finally: | ||
Gael Varoquaux
|
r1362 | self.after_execute() | ||
Fernando Perez
|
r1706 | |||
gvaroquaux
|
r1455 | #-------------------------------------------------------------------------- | ||
# LineFrontEndBase interface | ||||
#-------------------------------------------------------------------------- | ||||
def prefilter_input(self, string): | ||||
gvaroquaux
|
r1630 | """ Prefilter the input to turn it in valid python. | ||
gvaroquaux
|
r1455 | """ | ||
string = string.replace('\r\n', '\n') | ||||
string = string.replace('\t', 4*' ') | ||||
# Clean the trailing whitespace | ||||
string = '\n'.join(l.rstrip() for l in string.split('\n')) | ||||
return string | ||||
Gael Varoquaux
|
r1362 | |||
gvaroquaux
|
r1463 | |||
Gael Varoquaux
|
r1362 | def after_execute(self): | ||
""" All the operations required after an execution to put the | ||||
terminal back in a shape where it is usable. | ||||
""" | ||||
self.prompt_number += 1 | ||||
Gael Varoquaux
|
r1458 | self.new_prompt(self.input_prompt_template.substitute( | ||
number=(self.last_result['number'] + 1))) | ||||
Gael Varoquaux
|
r1362 | # Start a new empty history entry | ||
self._add_history(None, '') | ||||
Gael Varoquaux
|
r1371 | self.history_cursor = len(self.history.input_cache) - 1 | ||
Gael Varoquaux
|
r1360 | |||
gvaroquaux
|
r1463 | def complete_current_input(self): | ||
""" Do code completion on current line. | ||||
""" | ||||
if self.debug: | ||||
print >>sys.__stdout__, "complete_current_input", | ||||
line = self.input_buffer | ||||
new_line, completions = self.complete(line) | ||||
if len(completions)>1: | ||||
Gael Varoquaux
|
r1624 | self.write_completion(completions, new_line=new_line) | ||
gvaroquaux
|
r1629 | elif not line == new_line: | ||
gvaroquaux
|
r1628 | self.input_buffer = new_line | ||
gvaroquaux
|
r1463 | if self.debug: | ||
Gael Varoquaux
|
r1624 | print >>sys.__stdout__, 'line', line | ||
print >>sys.__stdout__, 'new_line', new_line | ||||
gvaroquaux
|
r1463 | print >>sys.__stdout__, completions | ||
gvaroquaux
|
r1464 | def get_line_width(self): | ||
""" Return the width of the line in characters. | ||||
""" | ||||
return 80 | ||||
Gael Varoquaux
|
r1624 | def write_completion(self, possibilities, new_line=None): | ||
gvaroquaux
|
r1463 | """ Write the list of possible completions. | ||
Gael Varoquaux
|
r1624 | |||
new_line is the completed input line that should be displayed | ||||
after the completion are writen. If None, the input_buffer | ||||
before the completion is used. | ||||
gvaroquaux
|
r1463 | """ | ||
Gael Varoquaux
|
r1624 | if new_line is None: | ||
new_line = self.input_buffer | ||||
gvaroquaux
|
r1463 | |||
self.write('\n') | ||||
max_len = len(max(possibilities, key=len)) + 1 | ||||
gvaroquaux
|
r1464 | # Now we check how much symbol we can put on a line... | ||
chars_per_line = self.get_line_width() | ||||
gvaroquaux
|
r1463 | symbols_per_line = max(1, chars_per_line/max_len) | ||
pos = 1 | ||||
Gael Varoquaux
|
r1887 | completion_string = [] | ||
gvaroquaux
|
r1463 | for symbol in possibilities: | ||
if pos < symbols_per_line: | ||||
Gael Varoquaux
|
r1887 | completion_string.append(symbol.ljust(max_len)) | ||
gvaroquaux
|
r1463 | pos += 1 | ||
else: | ||||
Gael Varoquaux
|
r1887 | completion_string.append(symbol.rstrip() + '\n') | ||
gvaroquaux
|
r1463 | pos = 1 | ||
Gael Varoquaux
|
r1887 | self.write(''.join(completion_string)) | ||
gvaroquaux
|
r1463 | self.new_prompt(self.input_prompt_template.substitute( | ||
number=self.last_result['number'] + 1)) | ||||
Gael Varoquaux
|
r1624 | self.input_buffer = new_line | ||
gvaroquaux
|
r1463 | |||
def new_prompt(self, prompt): | ||||
""" Prints a prompt and starts a new editing buffer. | ||||
Subclasses should use this method to make sure that the | ||||
terminal is put in a state favorable for a new line | ||||
input. | ||||
""" | ||||
self.input_buffer = '' | ||||
self.write(prompt) | ||||
Gael Varoquaux
|
r1884 | def continuation_prompt(self): | ||
"""Returns the current continuation prompt. | ||||
Gael Varoquaux
|
r1893 | """ | ||
return ("."*(len(self.last_prompt)-2) + ': ') | ||||
Gael Varoquaux
|
r1884 | |||
Gael Varoquaux
|
r1712 | def execute_command(self, command, hidden=False): | ||
""" Execute a command, not only in the model, but also in the | ||||
view, if any. | ||||
""" | ||||
return self.shell.execute(command) | ||||
gvaroquaux
|
r1455 | #-------------------------------------------------------------------------- | ||
# Private API | ||||
#-------------------------------------------------------------------------- | ||||
Gael Varoquaux
|
r1896 | def _on_enter(self, new_line_pos=0): | ||
Gael Varoquaux
|
r1355 | """ Called when the return key is pressed in a line editing | ||
buffer. | ||||
Gael Varoquaux
|
r1896 | |||
Parameters | ||||
---------- | ||||
new_line_pos : integer, optional | ||||
Position of the new line to add, starting from the | ||||
end (0 adds a new line after the last line, -1 before | ||||
the last line...) | ||||
Returns | ||||
------- | ||||
True if execution is triggered | ||||
Gael Varoquaux
|
r1355 | """ | ||
gvaroquaux
|
r1462 | current_buffer = self.input_buffer | ||
Gael Varoquaux
|
r1884 | # XXX: This string replace is ugly, but there should be no way it | ||
# fails. | ||||
prompt_less_buffer = re.sub('^' + self.continuation_prompt(), | ||||
'', current_buffer).replace('\n' + self.continuation_prompt(), | ||||
'\n') | ||||
cleaned_buffer = self.prefilter_input(prompt_less_buffer) | ||||
Gael Varoquaux
|
r1371 | if self.is_complete(cleaned_buffer): | ||
Gael Varoquaux
|
r1360 | self.execute(cleaned_buffer, raw_string=current_buffer) | ||
Gael Varoquaux
|
r1896 | return True | ||
Gael Varoquaux
|
r1355 | else: | ||
Gael Varoquaux
|
r1947 | # Start a new line. | ||
Gael Varoquaux
|
r1896 | new_line_pos = -new_line_pos | ||
lines = current_buffer.split('\n')[:-1] | ||||
prompt_less_lines = prompt_less_buffer.split('\n') | ||||
Gael Varoquaux
|
r1947 | # Create the new line, with the continuation prompt, and the | ||
# same amount of indent than the line above it. | ||||
Gael Varoquaux
|
r1896 | new_line = self.continuation_prompt() + \ | ||
self._get_indent_string('\n'.join( | ||||
prompt_less_lines[:new_line_pos-1])) | ||||
Gael Varoquaux
|
r1899 | if len(lines) == 1: | ||
Gael Varoquaux
|
r1947 | # We are starting a first continuation line. Indent it. | ||
Gael Varoquaux
|
r1896 | new_line += '\t' | ||
Gael Varoquaux
|
r1899 | elif current_buffer[:-1].split('\n')[-1].rstrip().endswith(':'): | ||
Gael Varoquaux
|
r1947 | # The last line ends with ":", autoindent the new line. | ||
Gael Varoquaux
|
r1896 | new_line += '\t' | ||
Gael Varoquaux
|
r1355 | |||
Gael Varoquaux
|
r1896 | if new_line_pos == 0: | ||
lines.append(new_line) | ||||
else: | ||||
lines.insert(new_line_pos, new_line) | ||||
self.input_buffer = '\n'.join(lines) | ||||
Gael Varoquaux
|
r1355 | |||
def _get_indent_string(self, string): | ||||
gvaroquaux
|
r1455 | """ Return the string of whitespace that prefixes a line. Used to | ||
add the right amount of indendation when creating a new line. | ||||
""" | ||||
Gael Varoquaux
|
r1362 | string = string.replace('\t', ' '*4) | ||
Gael Varoquaux
|
r1355 | string = string.split('\n')[-1] | ||
indent_chars = len(string) - len(string.lstrip()) | ||||
indent_string = '\t'*(indent_chars // 4) + \ | ||||
' '*(indent_chars % 4) | ||||
return indent_string | ||||