diff --git a/IPython/frontend/qt/console/completion_html.py b/IPython/frontend/qt/console/completion_html.py new file mode 100644 index 0000000..852fcf3 --- /dev/null +++ b/IPython/frontend/qt/console/completion_html.py @@ -0,0 +1,371 @@ +"""a navigable completer for the qtconsole""" +# coding : utf-8 +#----------------------------------------------------------------------------- +# Copyright (c) 2012, IPython Development Team.$ +# +# Distributed under the terms of the Modified BSD License.$ +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + +# System library imports +import IPython.utils.text as text + +from IPython.external.qt import QtCore, QtGui + +#-------------------------------------------------------------------------- +# Return an HTML table with selected item in a special class +#-------------------------------------------------------------------------- +def html_tableify(item_matrix, select=None, header=None , footer=None) : + """ returnr a string for an html table""" + if not item_matrix : + return '' + html_cols = [] + tds = lambda text : u''+text+u' ' + trs = lambda text : u''+text+u'' + tds_items = [map(tds, row) for row in item_matrix] + if select : + row, col = select + tds_items[row][col] = u''\ + +item_matrix[row][col]\ + +u' ' + #select the right item + html_cols = map(trs, (u''.join(row) for row in tds_items)) + head = '' + foot = '' + if header : + head = (u''\ + +''.join((u''+header+u'')*len(item_matrix[0]))\ + +'') + + if footer : + foot = (u''\ + +''.join((u''+footer+u'')*len(item_matrix[0]))\ + +'') + html = (u''+head+(u''.join(html_cols))+foot+u'
') + return html + +class SlidingInterval(object): + """a bound interval that follows a cursor + + internally used to scoll the completion view when the cursor + try to go beyond the edges, and show '...' when rows are hidden + """ + + _min = 0 + _max = 1 + _current = 0 + def __init__(self, maximum=1, width=6, minimum=0, sticky_lenght=1): + """Create a new bounded interval + + any value return by this will be bound between maximum and + minimum. usual width will be 'width', and sticky_length + set when the return interval should expand to max and min + """ + self._min = minimum + self._max = maximum + self._start = 0 + self._width = width + self._stop = self._start+self._width+1 + self._sticky_lenght = sticky_lenght + + @property + def current(self): + """current cursor position""" + return self._current + + @current.setter + def current(self, value): + """set current cursor position""" + current = min(max(self._min, value), self._max) + + self._current = current + + if current > self._stop : + self._stop = current + self._start = current-self._width + elif current < self._start : + self._start = current + self._stop = current + self._width + + if abs(self._start - self._min) <= self._sticky_lenght : + self._start = self._min + + if abs(self._stop - self._max) <= self._sticky_lenght : + self._stop = self._max + + @property + def start(self): + """begiiing of interval to show""" + return self._start + + @property + def stop(self): + """end of interval to show""" + return self._stop + + @property + def width(self): + return self._stop - self._start + + @property + def nth(self): + return self.current - self.start + +class CompletionHtml(QtGui.QWidget): + """ A widget for tab completion, navigable by arrow keys """ + + #-------------------------------------------------------------------------- + # 'QObject' interface + #-------------------------------------------------------------------------- + + _items = () + _index = (0, 0) + _consecutive_tab = 0 + _size = (1, 1) + _old_cursor = None + _start_position = 0 + _slice_start = 0 + _slice_len = 4 + + def __init__(self, console_widget): + """ Create a completion widget that is attached to the specified Qt + text edit widget. + """ + assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) + super(CompletionHtml, self).__init__() + + self._text_edit = console_widget._control + self._console_widget = console_widget + self._text_edit.installEventFilter(self) + self._sliding_interval = None + self._justified_items = None + + # Ensure that the text edit keeps focus when widget is displayed. + self.setFocusProxy(self._text_edit) + + + def eventFilter(self, obj, event): + """ Reimplemented to handle keyboard input and to auto-hide when the + text edit loses focus. + """ + if obj == self._text_edit: + etype = event.type() + if etype == QtCore.QEvent.KeyPress: + key = event.key() + if self._consecutive_tab == 0 and key in (QtCore.Qt.Key_Tab,): + return False + elif self._consecutive_tab == 1 and key in (QtCore.Qt.Key_Tab,): + # ok , called twice, we grab focus, and show the cursor + self._consecutive_tab = self._consecutive_tab+1 + self._update_list() + return True + elif self._consecutive_tab == 2: + if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): + self._complete_current() + return True + if key in (QtCore.Qt.Key_Tab,): + self.select_right() + self._update_list() + return True + elif key in ( QtCore.Qt.Key_Down,): + self.select_down() + self._update_list() + return True + elif key in (QtCore.Qt.Key_Right,): + self.select_right() + self._update_list() + return True + elif key in ( QtCore.Qt.Key_Up,): + self.select_up() + self._update_list() + return True + elif key in ( QtCore.Qt.Key_Left,): + self.select_left() + self._update_list() + return True + elif key in ( QtCore.Qt.Key_Escape,): + self.cancel_completion() + return True + else : + self.cancel_completion() + else: + self.cancel_completion() + + elif etype == QtCore.QEvent.FocusOut: + self.cancel_completion() + + return super(CompletionHtml, self).eventFilter(obj, event) + + #-------------------------------------------------------------------------- + # 'CompletionHtml' interface + #-------------------------------------------------------------------------- + def cancel_completion(self): + """Cancel the completion + + should be called when the completer have to be dismissed + + This reset internal variable, clearing the temporary buffer + of the console where the completion are shown. + """ + self._consecutive_tab = 0 + self._slice_start = 0 + self._console_widget._clear_temporary_buffer() + self._index = (0, 0) + if(self._sliding_interval): + self._sliding_interval = None + + # + # ... 2 4 4 4 4 4 4 4 4 4 4 4 4 + # 2 2 4 4 4 4 4 4 4 4 4 4 4 4 + # + #2 2 x x x x x x x x x x x 5 5 + #6 6 x x x x x x x x x x x 5 5 + #6 6 x x x x x x x x x x ? 5 5 + #6 6 x x x x x x x x x x ? 1 1 + # + #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ... + #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ... + def _select_index(self, row, col): + """Change the selection index, and make sure it stays in the right range + + A little more complicated than just dooing modulo the number of row columns + to be sure to cycle through all element. + + horizontaly, the element are maped like this : + to r <-- a b c d e f --> to g + to f <-- g h i j k l --> to m + to l <-- m n o p q r --> to a + + and vertically + a d g j m p + b e h k n q + c f i l o r + """ + + nr, nc = self._size + nr = nr-1 + nc = nc-1 + + # case 1 + if (row > nr and col >= nc) or (row >= nr and col > nc): + self._select_index(0, 0) + # case 2 + elif (row <= 0 and col < 0) or (row < 0 and col <= 0): + self._select_index(nr, nc) + # case 3 + elif row > nr : + self._select_index(0, col+1) + # case 4 + elif row < 0 : + self._select_index(nr, col-1) + # case 5 + elif col > nc : + self._select_index(row+1, 0) + # case 6 + elif col < 0 : + self._select_index(row-1, nc) + elif 0 <= row and row <= nr and 0 <= col and col <= nc : + self._index = (row, col) + else : + raise NotImplementedError("you'r trying to go where no completion\ + have gone before : %d:%d (%d:%d)"%(row, col, nr, nc) ) + + + @property + def _slice_end(self): + end = self._slice_start+self._slice_len + if end > len(self._items) : + return None + return end + + def select_up(self): + """move cursor up""" + r, c = self._index + self._select_index(r-1, c) + + def select_down(self): + """move cursor down""" + r, c = self._index + self._select_index(r+1, c) + + def select_left(self): + """move cursor left""" + r, c = self._index + self._select_index(r, c-1) + + def select_right(self): + """move cursor right""" + r, c = self._index + self._select_index(r, c+1) + + def show_items(self, cursor, items): + """ Shows the completion widget with 'items' at the position specified + by 'cursor'. + """ + if not items : + return + self._start_position = cursor.position() + self._consecutive_tab = 1 + items_m, ci = text.compute_item_matrix(items, empty=' ') + self._sliding_interval = SlidingInterval(len(items_m)-1) + + self._items = items_m + self._size = (ci['rows_numbers'], ci['columns_numbers']) + self._old_cursor = cursor + self._index = (0, 0) + sjoin = lambda x : [ y.ljust(w, ' ') for y, w in zip(x, ci['columns_width'])] + self._justified_items = map(sjoin, items_m) + self._update_list(hilight=False) + + + + + def _update_list(self, hilight=True): + """ update the list of completion and hilight the currently selected completion """ + self._sliding_interval.current = self._index[0] + head = None + foot = None + if self._sliding_interval.start > 0 : + head = '...' + + if self._sliding_interval.stop < self._sliding_interval._max: + foot = '...' + items_m = self._justified_items[\ + self._sliding_interval.start:\ + self._sliding_interval.stop+1\ + ] + + self._console_widget._clear_temporary_buffer() + if(hilight): + sel = (self._sliding_interval.nth, self._index[1]) + else : + sel = None + + strng = html_tableify(items_m, select=sel, header=head, footer=foot) + self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True) + + #-------------------------------------------------------------------------- + # Protected interface + #-------------------------------------------------------------------------- + + def _complete_current(self): + """ Perform the completion with the currently selected item. + """ + i = self._index + item = self._items[i[0]][i[1]] + item = item.strip() + if item : + self._current_text_cursor().insertText(item) + self.cancel_completion() + + def _current_text_cursor(self): + """ Returns a cursor with text between the start position and the + current position selected. + """ + cursor = self._text_edit.textCursor() + if cursor.position() >= self._start_position: + cursor.setPosition(self._start_position, + QtGui.QTextCursor.KeepAnchor) + return cursor + diff --git a/IPython/frontend/qt/console/completion_plain.py b/IPython/frontend/qt/console/completion_plain.py new file mode 100644 index 0000000..d6b4066 --- /dev/null +++ b/IPython/frontend/qt/console/completion_plain.py @@ -0,0 +1,62 @@ +"""a simple completer for the qtconsole""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012, IPython Development Team.$ +# +# Distributed under the terms of the Modified BSD License.$ +# +# The full license is in the file COPYING.txt, distributed with this software. +#------------------------------------------------------------------- + +# System library imports +from IPython.external.qt import QtCore, QtGui +import IPython.utils.text as text + + +class CompletionPlain(QtGui.QWidget): + """ A widget for tab completion, navigable by arrow keys """ + + #-------------------------------------------------------------------------- + # 'QObject' interface + #-------------------------------------------------------------------------- + + def __init__(self, console_widget): + """ Create a completion widget that is attached to the specified Qt + text edit widget. + """ + assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) + super(CompletionPlain, self).__init__() + + self._text_edit = console_widget._control + self._console_widget = console_widget + + self._text_edit.installEventFilter(self) + + def eventFilter(self, obj, event): + """ Reimplemented to handle keyboard input and to auto-hide when the + text edit loses focus. + """ + if obj == self._text_edit: + etype = event.type() + + if etype in( QtCore.QEvent.KeyPress, QtCore.QEvent.FocusOut ): + self.cancel_completion() + + return super(CompletionPlain, self).eventFilter(obj, event) + + #-------------------------------------------------------------------------- + # 'CompletionPlain' interface + #-------------------------------------------------------------------------- + def cancel_completion(self): + """Cancel the completion, reseting internal variable, clearing buffer """ + self._console_widget._clear_temporary_buffer() + + + def show_items(self, cursor, items): + """ Shows the completion widget with 'items' at the position specified + by 'cursor'. + """ + if not items : + return + self.cancel_completion() + strng = text.columnize(items) + self._console_widget._fill_temporary_buffer(cursor, strng, html=False) diff --git a/IPython/frontend/qt/console/completion_widget.py b/IPython/frontend/qt/console/completion_widget.py index 194b843..b258775 100644 --- a/IPython/frontend/qt/console/completion_widget.py +++ b/IPython/frontend/qt/console/completion_widget.py @@ -10,10 +10,11 @@ class CompletionWidget(QtGui.QListWidget): # 'QObject' interface #-------------------------------------------------------------------------- - def __init__(self, text_edit): + def __init__(self, console_widget): """ Create a completion widget that is attached to the specified Qt text edit widget. """ + text_edit = console_widget._control assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) super(CompletionWidget, self).__init__() @@ -132,3 +133,6 @@ class CompletionWidget(QtGui.QListWidget): self.hide() else: self.hide() + + def cancel_completion(self): + self.hide() diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index e4b3d6c..cd2a93c 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -5,7 +5,6 @@ #----------------------------------------------------------------------------- # Standard library imports -import os from os.path import commonprefix import re import sys @@ -23,6 +22,8 @@ from IPython.utils.text import columnize from IPython.utils.traitlets import Bool, Enum, Integer, Unicode from ansi_code_processor import QtAnsiCodeProcessor from completion_widget import CompletionWidget +from completion_html import CompletionHtml +from completion_plain import CompletionPlain from kill_ring import QtKillRing #----------------------------------------------------------------------------- @@ -65,10 +66,19 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): non-positive number disables text truncation (not recommended). """ ) - gui_completion = Bool(False, config=True, - help=""" - Use a list widget instead of plain text output for tab completion. - """ + gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True, + default_value = 'ncurses', + help=""" + The type of completer to use. Valid values are: + + 'plain' : Show the availlable completion as a text list + Below the editting area. + 'droplist': Show the completion in a drop down list navigable + by the arrow keys, and from which you can select + completion by pressing Return. + 'ncurses' : Show the completion as a text list which is navigable by + `tab` and arrow keys. + """ ) # NOTE: this value can only be specified during initialization. kind = Enum(['plain', 'rich'], default_value='plain', config=True, @@ -137,12 +147,12 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): font_changed = QtCore.Signal(QtGui.QFont) #------ Protected class variables ------------------------------------------ - + # control handles _control = None _page_control = None _splitter = None - + # When the control key is down, these keys are mapped. _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left, QtCore.Qt.Key_F : QtCore.Qt.Key_Right, @@ -161,6 +171,8 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O, QtCore.Qt.Key_V ]) + _temp_buffer_filled = False + #--------------------------------------------------------------------------- # 'QObject' interface #--------------------------------------------------------------------------- @@ -211,7 +223,13 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): # information for subclasses; they should be considered read-only. self._append_before_prompt_pos = 0 self._ansi_processor = QtAnsiCodeProcessor() - self._completion_widget = CompletionWidget(self._control) + if self.gui_completion == 'ncurses': + self._completion_widget = CompletionHtml(self) + elif self.gui_completion == 'droplist': + self._completion_widget = CompletionWidget(self) + elif self.gui_completion == 'plain': + self._completion_widget = CompletionPlain(self) + self._continuation_prompt = '> ' self._continuation_prompt_html = None self._executing = False @@ -228,7 +246,6 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): self._reading = False self._reading_callback = None self._tab_width = 8 - self._text_completing_pos = 0 # Set a monospaced font. self.reset_font() @@ -823,18 +840,17 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): """ self._append_custom(self._insert_plain_text, text, before_prompt) - def _cancel_text_completion(self): + def _cancel_completion(self): """ If text completion is progress, cancel it. """ - if self._text_completing_pos: - self._clear_temporary_buffer() - self._text_completing_pos = 0 + self._completion_widget.cancel_completion() def _clear_temporary_buffer(self): """ Clears the "temporary text" buffer, i.e. all the text following the prompt region. """ # Select and remove all text below the input buffer. + _temp_buffer_filled = False cursor = self._get_prompt_cursor() prompt = self._continuation_prompt.lstrip() while cursor.movePosition(QtGui.QTextCursor.NextBlock): @@ -862,7 +878,7 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): def _complete_with_items(self, cursor, items): """ Performs completion with 'items' at the specified cursor location. """ - self._cancel_text_completion() + self._cancel_completion() if len(items) == 1: cursor.setPosition(self._control.textCursor().position(), @@ -877,19 +893,26 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): cursor.insertText(prefix) current_pos = cursor.position() - if self.gui_completion: - cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix)) - self._completion_widget.show_items(cursor, items) - else: - cursor.beginEditBlock() - self._append_plain_text('\n') - self._page(self._format_as_columns(items)) - cursor.endEditBlock() + cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix)) + self._completion_widget.show_items(cursor, items) + + + def _fill_temporary_buffer(self, cursor, text, html=False): + """fill the area below the active editting zone with text""" + + current_pos = self._control.textCursor().position() + + cursor.beginEditBlock() + self._append_plain_text('\n') + self._page(text, html=html) + cursor.endEditBlock() + + cursor.setPosition(current_pos) + self._control.moveCursor(QtGui.QTextCursor.End) + self._control.setTextCursor(cursor) + + _temp_buffer_filled = True - cursor.setPosition(current_pos) - self._control.moveCursor(QtGui.QTextCursor.End) - self._control.setTextCursor(cursor) - self._text_completing_pos = current_pos def _context_menu_make(self, pos): """ Creates a context menu for the given QPoint (in widget coordinates). @@ -951,7 +974,6 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): control.viewport().installEventFilter(self) # Connect signals. - control.cursorPositionChanged.connect(self._cursor_position_changed) control.customContextMenuRequested.connect( self._custom_context_menu_requested) control.copyAvailable.connect(self.copy_available) @@ -1021,7 +1043,7 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): intercepted = True # Special handling when tab completing in text mode. - self._cancel_text_completion() + self._cancel_completion() if self._in_buffer(position): # Special handling when a reading a line of raw input. @@ -1634,8 +1656,9 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): def _keyboard_quit(self): """ Cancels the current editing task ala Ctrl-G in Emacs. """ - if self._text_completing_pos: - self._cancel_text_completion() + if self._temp_buffer_filled : + self._cancel_completion() + self._clear_temporary_buffer() else: self.input_buffer = '' @@ -1853,24 +1876,6 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): if diff < 0 and document.blockCount() == document.maximumBlockCount(): scrollbar.setValue(scrollbar.value() + diff) - def _cursor_position_changed(self): - """ Clears the temporary buffer based on the cursor position. - """ - if self._text_completing_pos: - document = self._control.document() - if self._text_completing_pos < document.characterCount(): - cursor = self._control.textCursor() - pos = cursor.position() - text_cursor = self._control.textCursor() - text_cursor.setPosition(self._text_completing_pos) - if pos < self._text_completing_pos or \ - cursor.blockNumber() > text_cursor.blockNumber(): - self._clear_temporary_buffer() - self._text_completing_pos = 0 - else: - self._clear_temporary_buffer() - self._text_completing_pos = 0 - def _custom_context_menu_requested(self, pos): """ Shows a context menu at the given QPoint (in widget coordinates). """ diff --git a/IPython/frontend/qt/console/qtconsoleapp.py b/IPython/frontend/qt/console/qtconsoleapp.py index 5824607..1735d9b 100644 --- a/IPython/frontend/qt/console/qtconsoleapp.py +++ b/IPython/frontend/qt/console/qtconsoleapp.py @@ -104,11 +104,7 @@ qt_flags = { 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}}, "Disable rich text support."), } -qt_flags.update(boolean_flag( - 'gui-completion', 'ConsoleWidget.gui_completion', - "use a GUI widget for tab completion", - "use plaintext output for completion" -)) + # and app_flags from the Console Mixin qt_flags.update(app_flags) # add frontend flags to the full set @@ -117,7 +113,6 @@ flags.update(qt_flags) # start with copy of front&backend aliases list aliases = dict(aliases) qt_aliases = dict( - style = 'IPythonWidget.syntax_style', stylesheet = 'IPythonQtConsoleApp.stylesheet', colors = 'ZMQInteractiveShell.colors', @@ -127,6 +122,7 @@ qt_aliases = dict( ) # and app_aliases from the Console Mixin qt_aliases.update(app_aliases) +qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'}) # add frontend aliases to the full set aliases.update(qt_aliases) diff --git a/IPython/frontend/qt/console/styles.py b/IPython/frontend/qt/console/styles.py index c8e2ba7..c72808f 100644 --- a/IPython/frontend/qt/console/styles.py +++ b/IPython/frontend/qt/console/styles.py @@ -22,6 +22,7 @@ default_light_style_template = ''' .in-prompt-number { font-weight: bold; } .out-prompt { color: darkred; } .out-prompt-number { font-weight: bold; } + .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;} ''' default_light_style_sheet = default_light_style_template%dict( bgcolor='white', fgcolor='black', select="#ccc") @@ -38,6 +39,7 @@ default_dark_style_template = ''' .in-prompt-number { color: lime; font-weight: bold; } .out-prompt { color: red; } .out-prompt-number { color: red; font-weight: bold; } + .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;} ''' default_dark_style_sheet = default_dark_style_template%dict( bgcolor='black', fgcolor='white', select="#555") @@ -50,6 +52,7 @@ default_bw_style_sheet = ''' selection-background-color: #cccccc} .in-prompt-number { font-weight: bold; } .out-prompt-number { font-weight: bold; } + .inverted { background-color: black ; color: white;} ''' default_bw_syntax_style = 'bw' diff --git a/IPython/utils/tests/test_text.py b/IPython/utils/tests/test_text.py index ce6e3d0..36b6075 100644 --- a/IPython/utils/tests/test_text.py +++ b/IPython/utils/tests/test_text.py @@ -14,6 +14,7 @@ import os import math +import random import nose.tools as nt @@ -32,13 +33,37 @@ def test_columnize(): items = [l*size for l in 'abc'] out = text.columnize(items, displaywidth=80) nt.assert_equals(out, 'aaaaa bbbbb ccccc\n') - out = text.columnize(items, displaywidth=10) + out = text.columnize(items, displaywidth=12) nt.assert_equals(out, 'aaaaa ccccc\nbbbbb\n') - + out = text.columnize(items, displaywidth=10) + nt.assert_equals(out, 'aaaaa\nbbbbb\nccccc\n') + +def test_columnize_random(): + """Test with random input to hopfully catch edge case """ + for nitems in [random.randint(2,70) for i in range(2,20)]: + displaywidth = random.randint(20,200) + rand_len = [random.randint(2,displaywidth) for i in range(nitems)] + items = ['x'*l for l in rand_len] + out = text.columnize(items, displaywidth=displaywidth) + longer_line = max([len(x) for x in out.split('\n')]) + longer_element = max(rand_len) + if longer_line > displaywidth: + print "Columnize displayed something lager than displaywidth : %s " % longer_line + print "longer element : %s " % longer_element + print "displaywidth : %s " % displaywidth + print "number of element : %s " % nitems + print "size of each element :\n %s" % rand_len + assert False + +def test_columnize_medium(): + """Test with inputs than shouldn't be wider tahn 80 """ + size = 40 + items = [l*size for l in 'abc'] + out = text.columnize(items, displaywidth=80) + nt.assert_equals(out, '\n'.join(items+[''])) def test_columnize_long(): """Test columnize with inputs longer than the display window""" - text.columnize(['a'*81, 'b'*81], displaywidth=80) size = 11 items = [l*size for l in 'abc'] out = text.columnize(items, displaywidth=size-1) diff --git a/IPython/utils/text.py b/IPython/utils/text.py index aa07f39..0df0bec 100644 --- a/IPython/utils/text.py +++ b/IPython/utils/text.py @@ -24,7 +24,7 @@ import textwrap from string import Formatter from IPython.external.path import path -from IPython.testing.skipdoctest import skip_doctest_py3 +from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest from IPython.utils import py3compat from IPython.utils.io import nlprint from IPython.utils.data import flatten @@ -660,6 +660,91 @@ class DollarFormatter(FullEvalFormatter): # Re-yield the {foo} style pattern yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion) +#----------------------------------------------------------------------------- +# Utils to columnize a list of string +#----------------------------------------------------------------------------- +def _chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in xrange(0, len(l), n): + yield l[i:i+n] + +def _find_optimal(rlist , separator_size=2 , displaywidth=80): + """Calculate optimal info to columnize a list of string""" + for nrow in range(1, len(rlist)+1) : + chk = map(max,_chunks(rlist, nrow)) + sumlength = sum(chk) + ncols = len(chk) + if sumlength+separator_size*(ncols-1) <= displaywidth : + break; + return {'columns_numbers' : ncols, + 'optimal_separator_width':(displaywidth - sumlength)/(ncols-1) if (ncols -1) else 0, + 'rows_numbers' : nrow, + 'columns_width' : chk + } + +def _get_or_default(mylist, i, default=None): + """return list item number, or default if don't exist""" + if i >= len(mylist): + return default + else : + return mylist[i] + +@skip_doctest +def compute_item_matrix(items, empty=None, *args, **kwargs) : + """Returns a nested list, and info to columnize items + + Parameters : + ------------ + + items : + list of strings to columize + empty : (default None) + default value to fill list if needed + separator_size : int (default=2) + How much caracters will be used as a separation between each columns. + displaywidth : int (default=80) + The width of the area onto wich the columns should enter + + Returns : + --------- + + Returns a tuple of (strings_matrix, dict_info) + + strings_matrix : + + nested list of string, the outer most list contains as many list as + rows, the innermost lists have each as many element as colums. If the + total number of elements in `items` does not equal the product of + rows*columns, the last element of some lists are filled with `None`. + + dict_info : + some info to make columnize easier: + + columns_numbers : number of columns + rows_numbers : number of rows + columns_width : list of with of each columns + optimal_separator_width : best separator width between columns + + Exemple : + --------- + + In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l'] + ...: compute_item_matrix(l,displaywidth=12) + Out[1]: + ([['aaa', 'f', 'k'], + ['b', 'g', 'l'], + ['cc', 'h', None], + ['d', 'i', None], + ['eeeee', 'j', None]], + {'columns_numbers': 3, + 'columns_width': [5, 1, 1], + 'optimal_separator_width': 2, + 'rows_numbers': 5}) + + """ + info = _find_optimal(map(len, items), *args, **kwargs) + nrow, ncol = info['rows_numbers'], info['columns_numbers'] + return ([[ _get_or_default(items, c*nrow+i, default=empty) for c in range(ncol) ] for i in range(nrow) ], info) def columnize(items, separator=' ', displaywidth=80): """ Transform a list of strings into a single string with columns. @@ -679,58 +764,9 @@ def columnize(items, separator=' ', displaywidth=80): ------- The formatted string. """ - # Note: this code is adapted from columnize 0.3.2. - # See http://code.google.com/p/pycolumnize/ - - # Some degenerate cases. - size = len(items) - if size == 0: + if not items : return '\n' - elif size == 1: - return '%s\n' % items[0] - - # Special case: if any item is longer than the maximum width, there's no - # point in triggering the logic below... - item_len = map(len, items) # save these, we can reuse them below - longest = max(item_len) - if longest >= displaywidth: - return '\n'.join(items+['']) - - # Try every row count from 1 upwards - array_index = lambda nrows, row, col: nrows*col + row - for nrows in range(1, size): - ncols = (size + nrows - 1) // nrows - colwidths = [] - totwidth = -len(separator) - for col in range(ncols): - # Get max column width for this column - colwidth = 0 - for row in range(nrows): - i = array_index(nrows, row, col) - if i >= size: break - x, len_x = items[i], item_len[i] - colwidth = max(colwidth, len_x) - colwidths.append(colwidth) - totwidth += colwidth + len(separator) - if totwidth > displaywidth: - break - if totwidth <= displaywidth: - break - - # The smallest number of rows computed and the max widths for each - # column has been obtained. Now we just have to format each of the rows. - string = '' - for row in range(nrows): - texts = [] - for col in range(ncols): - i = row + nrows*col - if i >= size: - texts.append('') - else: - texts.append(items[i]) - while texts and not texts[-1]: - del texts[-1] - for col in range(len(texts)): - texts[col] = texts[col].ljust(colwidths[col]) - string += '%s\n' % separator.join(texts) - return string + matrix, info = compute_item_matrix(items, separator_size=len(separator), displaywidth=displaywidth) + fmatrix = [filter(None, x) for x in matrix] + sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['columns_width'])]) + return '\n'.join(map(sjoin, fmatrix))+'\n'