Fix columnize bug, where tab completion with very long filenames would crash Qt console #696

Merged
merged 3 commits into from Aug 17, 2011
@@ -19,6 +19,7 @@
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 @@ def _format_as_columns(self, items, separator=' '):
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.
@@ -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+['']))
View
@@ -611,3 +611,76 @@ def get_value(self, key, args, kwargs):
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