Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from Aug 17, 2011
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 2 additions & 46 deletions IPython/frontend/qt/console/console_widget.py
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
44 changes: 44 additions & 0 deletions 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+['']))
73 changes: 73 additions & 0 deletions IPython/utils/text.py
Expand Up @@ -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