From aa7247e8fa25866849a8349eefde5226e19464a1 2011-08-17 07:17:21 From: Fernando Perez Date: 2011-08-17 07:17:21 Subject: [PATCH] Merge pull request #696 from fperez/fix_columnize Fix columnize bug, where tab completion with very long filenames would crash Qt console. Thanks to Mani Chandra for reporting on list and Julian Taylor for the script to reproduce the crash. --- diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index 64cb433..b029cf0 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -19,6 +19,7 @@ from IPython.external.qt import QtCore, QtGui from IPython.config.configurable import Configurable from IPython.frontend.qt.rich_text import HtmlExporter from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font +from IPython.utils.text import columnize from IPython.utils.traitlets import Bool, Enum, Int, Unicode from ansi_code_processor import QtAnsiCodeProcessor from completion_widget import CompletionWidget @@ -1295,52 +1296,7 @@ class ConsoleWidget(Configurable, QtGui.QWidget): width = self._control.viewport().width() char_width = QtGui.QFontMetrics(self.font).width(' ') displaywidth = max(10, (width / char_width) - 1) - - # Some degenerate cases. - size = len(items) - if size == 0: - return '\n' - elif size == 1: - return '%s\n' % items[0] - - # 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 = items[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 + return columnize(items, separator, displaywidth) def _get_block_plain_text(self, block): """ Given a QTextBlock, return its unformatted text. diff --git a/IPython/utils/tests/test_text.py b/IPython/utils/tests/test_text.py new file mode 100644 index 0000000..0fa23cb --- /dev/null +++ b/IPython/utils/tests/test_text.py @@ -0,0 +1,44 @@ +# encoding: utf-8 +"""Tests for IPython.utils.text""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2011 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 os + +import nose.tools as nt + +from nose import with_setup + +from IPython.testing import decorators as dec +from IPython.utils import text + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- + +def test_columnize(): + """Basic columnize tests.""" + size = 5 + 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) + nt.assert_equals(out, 'aaaaa ccccc\nbbbbb\n') + + +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) + nt.assert_equals(out, '\n'.join(items+[''])) diff --git a/IPython/utils/text.py b/IPython/utils/text.py index d0e3387..a23905f 100644 --- a/IPython/utils/text.py +++ b/IPython/utils/text.py @@ -611,3 +611,76 @@ class EvalFormatter(Formatter): raise KeyError(key) +def columnize(items, separator=' ', displaywidth=80): + """ Transform a list of strings into a single string with columns. + + Parameters + ---------- + items : sequence of strings + The strings to process. + + separator : str, optional [default is two spaces] + The string that separates columns. + + displaywidth : int, optional [default is 80] + Width of the display in number of characters. + + Returns + ------- + 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: + 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