text.py
724 lines
| 21.3 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2498 | # encoding: utf-8 | ||
""" | ||||
Utilities for working with strings and text. | ||||
Thomas Kluyver
|
r8795 | |||
Inheritance diagram: | ||||
.. inheritance-diagram:: IPython.utils.text | ||||
:parts: 3 | ||||
Brian Granger
|
r2498 | """ | ||
#----------------------------------------------------------------------------- | ||||
Matthias BUSSONNIER
|
r5390 | # Copyright (C) 2008-2011 The IPython Development Team | ||
Brian Granger
|
r2498 | # | ||
# 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 re | ||||
Thomas Kluyver
|
r12413 | import sys | ||
MinRK
|
r4020 | import textwrap | ||
MinRK
|
r4004 | from string import Formatter | ||
Brian Granger
|
r2498 | |||
from IPython.external.path import path | ||||
Matthias BUSSONNIER
|
r7388 | from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest | ||
Thomas Kluyver
|
r4732 | from IPython.utils import py3compat | ||
Brian Granger
|
r2498 | |||
#----------------------------------------------------------------------------- | ||||
Thomas Kluyver
|
r12413 | # Declarations | ||
#----------------------------------------------------------------------------- | ||||
# datetime.strftime date format for ipython | ||||
if sys.platform == 'win32': | ||||
date_format = "%B %d, %Y" | ||||
else: | ||||
date_format = "%B %-d, %Y" | ||||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r2498 | # Code | ||
#----------------------------------------------------------------------------- | ||||
class LSString(str): | ||||
"""String derivative with a special access attributes. | ||||
These are normal strings, but with the special attributes: | ||||
.l (or .list) : value as list (split on newlines). | ||||
.n (or .nlstr): original value (the string itself). | ||||
.s (or .spstr): value as whitespace-separated string. | ||||
.p (or .paths): list of path objects | ||||
Any values which require transformations are computed only once and | ||||
cached. | ||||
Such strings are very useful to efficiently interact with the shell, which | ||||
typically only understands whitespace-separated options for commands.""" | ||||
def get_list(self): | ||||
try: | ||||
return self.__list | ||||
except AttributeError: | ||||
self.__list = self.split('\n') | ||||
return self.__list | ||||
l = list = property(get_list) | ||||
def get_spstr(self): | ||||
try: | ||||
return self.__spstr | ||||
except AttributeError: | ||||
self.__spstr = self.replace('\n',' ') | ||||
return self.__spstr | ||||
s = spstr = property(get_spstr) | ||||
def get_nlstr(self): | ||||
return self | ||||
n = nlstr = property(get_nlstr) | ||||
def get_paths(self): | ||||
try: | ||||
return self.__paths | ||||
except AttributeError: | ||||
self.__paths = [path(p) for p in self.split('\n') if os.path.exists(p)] | ||||
return self.__paths | ||||
p = paths = property(get_paths) | ||||
Brian Granger
|
r2781 | # FIXME: We need to reimplement type specific displayhook and then add this | ||
# back as a custom printer. This should also be moved outside utils into the | ||||
# core. | ||||
Brian Granger
|
r2498 | |||
Brian Granger
|
r2781 | # def print_lsstring(arg): | ||
# """ Prettier (non-repr-like) and more informative printer for LSString """ | ||||
# print "LSString (.p, .n, .l, .s available). Value:" | ||||
# print arg | ||||
Bernardo B. Marques
|
r4872 | # | ||
# | ||||
Brian Granger
|
r2781 | # print_lsstring = result_display.when_type(LSString)(print_lsstring) | ||
Brian Granger
|
r2498 | |||
class SList(list): | ||||
"""List derivative with a special access attributes. | ||||
These are normal lists, but with the special attributes: | ||||
.l (or .list) : value as list (the list itself). | ||||
.n (or .nlstr): value as a string, joined on newlines. | ||||
.s (or .spstr): value as a string, joined on spaces. | ||||
.p (or .paths): list of path objects | ||||
Any values which require transformations are computed only once and | ||||
cached.""" | ||||
def get_list(self): | ||||
return self | ||||
l = list = property(get_list) | ||||
def get_spstr(self): | ||||
try: | ||||
return self.__spstr | ||||
except AttributeError: | ||||
self.__spstr = ' '.join(self) | ||||
return self.__spstr | ||||
s = spstr = property(get_spstr) | ||||
def get_nlstr(self): | ||||
try: | ||||
return self.__nlstr | ||||
except AttributeError: | ||||
self.__nlstr = '\n'.join(self) | ||||
return self.__nlstr | ||||
n = nlstr = property(get_nlstr) | ||||
def get_paths(self): | ||||
try: | ||||
return self.__paths | ||||
except AttributeError: | ||||
self.__paths = [path(p) for p in self if os.path.exists(p)] | ||||
return self.__paths | ||||
p = paths = property(get_paths) | ||||
def grep(self, pattern, prune = False, field = None): | ||||
""" Return all strings matching 'pattern' (a regex or callable) | ||||
This is case-insensitive. If prune is true, return all items | ||||
NOT matching the pattern. | ||||
If field is specified, the match must occur in the specified | ||||
whitespace-separated field. | ||||
Examples:: | ||||
a.grep( lambda x: x.startswith('C') ) | ||||
a.grep('Cha.*log', prune=1) | ||||
a.grep('chm', field=-1) | ||||
""" | ||||
def match_target(s): | ||||
if field is None: | ||||
return s | ||||
parts = s.split() | ||||
try: | ||||
tgt = parts[field] | ||||
return tgt | ||||
except IndexError: | ||||
return "" | ||||
if isinstance(pattern, basestring): | ||||
pred = lambda x : re.search(pattern, x, re.IGNORECASE) | ||||
else: | ||||
pred = pattern | ||||
if not prune: | ||||
return SList([el for el in self if pred(match_target(el))]) | ||||
else: | ||||
return SList([el for el in self if not pred(match_target(el))]) | ||||
def fields(self, *fields): | ||||
""" Collect whitespace-separated fields from string list | ||||
Allows quick awk-like usage of string lists. | ||||
Example data (in var a, created by 'a = !ls -l'):: | ||||
-rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog | ||||
drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython | ||||
a.fields(0) is ['-rwxrwxrwx', 'drwxrwxrwx+'] | ||||
a.fields(1,0) is ['1 -rwxrwxrwx', '6 drwxrwxrwx+'] | ||||
(note the joining by space). | ||||
a.fields(-1) is ['ChangeLog', 'IPython'] | ||||
IndexErrors are ignored. | ||||
Without args, fields() just split()'s the strings. | ||||
""" | ||||
if len(fields) == 0: | ||||
return [el.split() for el in self] | ||||
res = SList() | ||||
for el in [f.split() for f in self]: | ||||
lineparts = [] | ||||
for fd in fields: | ||||
try: | ||||
lineparts.append(el[fd]) | ||||
except IndexError: | ||||
pass | ||||
if lineparts: | ||||
res.append(" ".join(lineparts)) | ||||
return res | ||||
def sort(self,field= None, nums = False): | ||||
""" sort by specified fields (see fields()) | ||||
Example:: | ||||
a.sort(1, nums = True) | ||||
Sorts a by second field, in numerical order (so that 21 > 3) | ||||
""" | ||||
#decorate, sort, undecorate | ||||
if field is not None: | ||||
dsu = [[SList([line]).fields(field), line] for line in self] | ||||
else: | ||||
dsu = [[line, line] for line in self] | ||||
if nums: | ||||
for i in range(len(dsu)): | ||||
numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()]) | ||||
try: | ||||
n = int(numstr) | ||||
except ValueError: | ||||
n = 0; | ||||
dsu[i][0] = n | ||||
dsu.sort() | ||||
return SList([t[1] for t in dsu]) | ||||
Brian Granger
|
r2781 | # FIXME: We need to reimplement type specific displayhook and then add this | ||
# back as a custom printer. This should also be moved outside utils into the | ||||
# core. | ||||
# def print_slist(arg): | ||||
# """ Prettier (non-repr-like) and more informative printer for SList """ | ||||
# print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):" | ||||
# if hasattr(arg, 'hideonce') and arg.hideonce: | ||||
# arg.hideonce = False | ||||
# return | ||||
Bernardo B. Marques
|
r4872 | # | ||
Thomas Kluyver
|
r9474 | # nlprint(arg) # This was a nested list printer, now removed. | ||
Bernardo B. Marques
|
r4872 | # | ||
Brian Granger
|
r2781 | # print_slist = result_display.when_type(SList)(print_slist) | ||
Brian Granger
|
r2498 | |||
MinRK
|
r3943 | def indent(instr,nspaces=4, ntabs=0, flatten=False): | ||
Brian Granger
|
r2498 | """Indent a string a given number of spaces or tabstops. | ||
indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3943 | Parameters | ||
---------- | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3943 | instr : basestring | ||
The string to be indented. | ||||
nspaces : int (default: 4) | ||||
The number of spaces to be indented. | ||||
ntabs : int (default: 0) | ||||
The number of tabs to be indented. | ||||
flatten : bool (default: False) | ||||
Whether to scrub existing indentation. If True, all lines will be | ||||
aligned to the same indentation. If False, existing indentation will | ||||
be strictly increased. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3943 | Returns | ||
------- | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3943 | str|unicode : string indented by ntabs and nspaces. | ||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2498 | """ | ||
MinRK
|
r3943 | if instr is None: | ||
Brian Granger
|
r2498 | return | ||
ind = '\t'*ntabs+' '*nspaces | ||||
MinRK
|
r3943 | if flatten: | ||
pat = re.compile(r'^\s*', re.MULTILINE) | ||||
else: | ||||
pat = re.compile(r'^', re.MULTILINE) | ||||
outstr = re.sub(pat, ind, instr) | ||||
Brian Granger
|
r2498 | if outstr.endswith(os.linesep+ind): | ||
return outstr[:-len(ind)] | ||||
else: | ||||
return outstr | ||||
def list_strings(arg): | ||||
"""Always return a list of strings, given a string or list of strings | ||||
as input. | ||||
:Examples: | ||||
In [7]: list_strings('A single string') | ||||
Out[7]: ['A single string'] | ||||
In [8]: list_strings(['A single string in a list']) | ||||
Out[8]: ['A single string in a list'] | ||||
In [9]: list_strings(['A','list','of','strings']) | ||||
Out[9]: ['A', 'list', 'of', 'strings'] | ||||
""" | ||||
if isinstance(arg,basestring): return [arg] | ||||
else: return arg | ||||
def marquee(txt='',width=78,mark='*'): | ||||
"""Return the input string centered in a 'marquee'. | ||||
:Examples: | ||||
In [16]: marquee('A test',40) | ||||
Out[16]: '**************** A test ****************' | ||||
In [17]: marquee('A test',40,'-') | ||||
Out[17]: '---------------- A test ----------------' | ||||
In [18]: marquee('A test',40,' ') | ||||
Out[18]: ' A test ' | ||||
""" | ||||
if not txt: | ||||
return (mark*width)[:width] | ||||
Thomas Kluyver
|
r4732 | nmark = (width-len(txt)-2)//len(mark)//2 | ||
Brian Granger
|
r2498 | if nmark < 0: nmark =0 | ||
marks = mark*nmark | ||||
return '%s %s %s' % (marks,txt,marks) | ||||
Brian Granger
|
r2761 | ini_spaces_re = re.compile(r'^(\s+)') | ||
def num_ini_spaces(strng): | ||||
"""Return the number of initial spaces in a string""" | ||||
ini_spaces = ini_spaces_re.match(strng) | ||||
if ini_spaces: | ||||
return ini_spaces.end() | ||||
else: | ||||
return 0 | ||||
Fernando Perez
|
r2929 | |||
def format_screen(strng): | ||||
"""Format a string for screen printing. | ||||
This removes some latex-type format codes.""" | ||||
# Paragraph continue | ||||
par_re = re.compile(r'\\$',re.MULTILINE) | ||||
strng = par_re.sub('',strng) | ||||
return strng | ||||
Fernando Perez
|
r7713 | |||
MinRK
|
r4020 | def dedent(text): | ||
"""Equivalent of textwrap.dedent that ignores unindented first line. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | This means it will still dedent strings like: | ||
'''foo | ||||
is a bar | ||||
''' | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | For use in wrap_paragraphs. | ||
""" | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | if text.startswith('\n'): | ||
# text starts with blank line, don't ignore the first line | ||||
return textwrap.dedent(text) | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | # split first line | ||
splits = text.split('\n',1) | ||||
if len(splits) == 1: | ||||
# only one line | ||||
return textwrap.dedent(text) | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | first, rest = splits | ||
# dedent everything but the first line | ||||
rest = textwrap.dedent(rest) | ||||
return '\n'.join([first, rest]) | ||||
Fernando Perez
|
r7713 | |||
MinRK
|
r4020 | def wrap_paragraphs(text, ncols=80): | ||
"""Wrap multiple paragraphs to fit a specified width. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | This is equivalent to textwrap.wrap, but with support for multiple | ||
paragraphs, as separated by empty lines. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | Returns | ||
------- | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | list of complete paragraphs, wrapped to fill `ncols` columns. | ||
""" | ||||
paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE) | ||||
text = dedent(text).strip() | ||||
paragraphs = paragraph_re.split(text)[::2] # every other entry is space | ||||
out_ps = [] | ||||
indent_re = re.compile(r'\n\s+', re.MULTILINE) | ||||
for p in paragraphs: | ||||
Bernardo B. Marques
|
r4872 | # presume indentation that survives dedent is meaningful formatting, | ||
MinRK
|
r4020 | # so don't fill unless text is flush. | ||
if indent_re.search(p) is None: | ||||
# wrap paragraph | ||||
p = textwrap.fill(p, ncols) | ||||
out_ps.append(p) | ||||
return out_ps | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | |||
Fernando Perez
|
r7713 | def long_substr(data): | ||
"""Return the longest common substring in a list of strings. | ||||
Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python | ||||
""" | ||||
substr = '' | ||||
if len(data) > 1 and len(data[0]) > 0: | ||||
for i in range(len(data[0])): | ||||
for j in range(len(data[0])-i+1): | ||||
if j > len(substr) and all(data[0][i:i+j] in x for x in data): | ||||
substr = data[0][i:i+j] | ||||
Bradley M. Froehle
|
r8147 | elif len(data) == 1: | ||
Fernando Perez
|
r7715 | substr = data[0] | ||
Fernando Perez
|
r7713 | return substr | ||
def strip_email_quotes(text): | ||||
"""Strip leading email quotation characters ('>'). | ||||
Fernando Perez
|
r7719 | |||
Removes any combination of leading '>' interspersed with whitespace that | ||||
appears *identically* in all lines of the input text. | ||||
Parameters | ||||
---------- | ||||
text : str | ||||
Examples | ||||
-------- | ||||
Simple uses:: | ||||
In [2]: strip_email_quotes('> > text') | ||||
Out[2]: 'text' | ||||
Fernando Perez
|
r7720 | In [3]: strip_email_quotes('> > text\\n> > more') | ||
Out[3]: 'text\\nmore' | ||||
Fernando Perez
|
r7719 | |||
Note how only the common prefix that appears in all lines is stripped:: | ||||
Fernando Perez
|
r7720 | In [4]: strip_email_quotes('> > text\\n> > more\\n> more...') | ||
Out[4]: '> text\\n> more\\nmore...' | ||||
Fernando Perez
|
r7719 | |||
So if any line has no quote marks ('>') , then none are stripped from any | ||||
of them :: | ||||
Fernando Perez
|
r7720 | In [5]: strip_email_quotes('> > text\\n> > more\\nlast different') | ||
Out[5]: '> > text\\n> > more\\nlast different' | ||||
Fernando Perez
|
r7713 | """ | ||
lines = text.splitlines() | ||||
matches = set() | ||||
for line in lines: | ||||
prefix = re.match(r'^(\s*>[ >]*)', line) | ||||
if prefix: | ||||
matches.add(prefix.group(1)) | ||||
else: | ||||
break | ||||
else: | ||||
prefix = long_substr(list(matches)) | ||||
if prefix: | ||||
strip = len(prefix) | ||||
text = '\n'.join([ ln[strip:] for ln in lines]) | ||||
return text | ||||
MinRK
|
r4004 | class EvalFormatter(Formatter): | ||
"""A String Formatter that allows evaluation of simple expressions. | ||||
Thomas Kluyver
|
r5353 | |||
Note that this version interprets a : as specifying a format string (as per | ||||
standard string formatting), so if slicing is required, you must explicitly | ||||
create a slice. | ||||
MinRK
|
r4004 | This is to be used in templating cases, such as the parallel batch | ||
script templates, where simple arithmetic on arguments is useful. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4004 | Examples | ||
-------- | ||||
Thomas Kluyver
|
r5353 | |||
In [1]: f = EvalFormatter() | ||||
In [2]: f.format('{n//4}', n=8) | ||||
Out [2]: '2' | ||||
In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello") | ||||
Out [3]: 'll' | ||||
""" | ||||
def get_field(self, name, args, kwargs): | ||||
v = eval(name, kwargs) | ||||
return v, name | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r7719 | |||
Thomas Kluyver
|
r5355 | @skip_doctest_py3 | ||
Thomas Kluyver
|
r5353 | class FullEvalFormatter(Formatter): | ||
"""A String Formatter that allows evaluation of simple expressions. | ||||
Any time a format key is not found in the kwargs, | ||||
it will be tried as an expression in the kwargs namespace. | ||||
Note that this version allows slicing using [1:2], so you cannot specify | ||||
a format string. Use :class:`EvalFormatter` to permit format strings. | ||||
Examples | ||||
-------- | ||||
In [1]: f = FullEvalFormatter() | ||||
Thomas Kluyver
|
r4764 | In [2]: f.format('{n//4}', n=8) | ||
Thomas Kluyver
|
r5355 | Out[2]: u'2' | ||
Thomas Kluyver
|
r5353 | |||
In [3]: f.format('{list(range(5))[2:4]}') | ||||
Thomas Kluyver
|
r5355 | Out[3]: u'[2, 3]' | ||
MinRK
|
r4004 | |||
In [4]: f.format('{3*2}') | ||||
Thomas Kluyver
|
r5355 | Out[4]: u'6' | ||
MinRK
|
r4004 | """ | ||
MinRK
|
r4654 | # copied from Formatter._vformat with minor changes to allow eval | ||
# and replace the format_spec code with slicing | ||||
def _vformat(self, format_string, args, kwargs, used_args, recursion_depth): | ||||
if recursion_depth < 0: | ||||
raise ValueError('Max string recursion exceeded') | ||||
result = [] | ||||
for literal_text, field_name, format_spec, conversion in \ | ||||
self.parse(format_string): | ||||
# output the literal text | ||||
if literal_text: | ||||
result.append(literal_text) | ||||
# if there's a field, output it | ||||
if field_name is not None: | ||||
# this is some markup, find the object and do | ||||
Thomas Kluyver
|
r5353 | # the formatting | ||
MinRK
|
r4654 | |||
Thomas Kluyver
|
r5353 | if format_spec: | ||
MinRK
|
r4654 | # override format spec, to allow slicing: | ||
field_name = ':'.join([field_name, format_spec]) | ||||
# eval the contents of the field for the object | ||||
# to be formatted | ||||
obj = eval(field_name, kwargs) | ||||
# do any conversion on the resulting object | ||||
obj = self.convert_field(obj, conversion) | ||||
# format the object and append to the result | ||||
Thomas Kluyver
|
r5353 | result.append(self.format_field(obj, '')) | ||
MinRK
|
r4654 | |||
Thomas Kluyver
|
r5355 | return u''.join(py3compat.cast_unicode(s) for s in result) | ||
MinRK
|
r4654 | |||
Fernando Perez
|
r7719 | |||
Thomas Kluyver
|
r5355 | @skip_doctest_py3 | ||
Thomas Kluyver
|
r5354 | class DollarFormatter(FullEvalFormatter): | ||
"""Formatter allowing Itpl style $foo replacement, for names and attribute | ||||
access only. Standard {foo} replacement also works, and allows full | ||||
evaluation of its arguments. | ||||
Examples | ||||
-------- | ||||
In [1]: f = DollarFormatter() | ||||
In [2]: f.format('{n//4}', n=8) | ||||
Thomas Kluyver
|
r5355 | Out[2]: u'2' | ||
Thomas Kluyver
|
r5354 | |||
In [3]: f.format('23 * 76 is $result', result=23*76) | ||||
Thomas Kluyver
|
r5355 | Out[3]: u'23 * 76 is 1748' | ||
Thomas Kluyver
|
r5354 | |||
In [4]: f.format('$a or {b}', a=1, b=2) | ||||
Thomas Kluyver
|
r5355 | Out[4]: u'1 or 2' | ||
Thomas Kluyver
|
r5354 | """ | ||
Thomas Kluyver
|
r5365 | _dollar_pattern = re.compile("(.*?)\$(\$?[\w\.]+)") | ||
Thomas Kluyver
|
r5354 | def parse(self, fmt_string): | ||
for literal_txt, field_name, format_spec, conversion \ | ||||
in Formatter.parse(self, fmt_string): | ||||
# Find $foo patterns in the literal text. | ||||
continue_from = 0 | ||||
Thomas Kluyver
|
r5365 | txt = "" | ||
Thomas Kluyver
|
r5354 | for m in self._dollar_pattern.finditer(literal_txt): | ||
new_txt, new_field = m.group(1,2) | ||||
Thomas Kluyver
|
r5365 | # $$foo --> $foo | ||
if new_field.startswith("$"): | ||||
txt += new_txt + new_field | ||||
else: | ||||
yield (txt + new_txt, new_field, "", None) | ||||
txt = "" | ||||
Thomas Kluyver
|
r5354 | continue_from = m.end() | ||
# Re-yield the {foo} style pattern | ||||
Thomas Kluyver
|
r5365 | yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion) | ||
MinRK
|
r4004 | |||
Matthias BUSSONNIER
|
r7387 | #----------------------------------------------------------------------------- | ||
# Utils to columnize a list of string | ||||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r7719 | |||
Matthias BUSSONNIER
|
r7387 | def _chunks(l, n): | ||
"""Yield successive n-sized chunks from l.""" | ||||
for i in xrange(0, len(l), n): | ||||
yield l[i:i+n] | ||||
Fernando Perez
|
r7719 | |||
Matthias BUSSONNIER
|
r7388 | def _find_optimal(rlist , separator_size=2 , displaywidth=80): | ||
Matthias BUSSONNIER
|
r7387 | """Calculate optimal info to columnize a list of string""" | ||
for nrow in range(1, len(rlist)+1) : | ||||
Matthias BUSSONNIER
|
r7388 | chk = map(max,_chunks(rlist, nrow)) | ||
Matthias BUSSONNIER
|
r7387 | sumlength = sum(chk) | ||
ncols = len(chk) | ||||
Matthias BUSSONNIER
|
r7388 | if sumlength+separator_size*(ncols-1) <= displaywidth : | ||
Matthias BUSSONNIER
|
r7387 | break; | ||
return {'columns_numbers' : ncols, | ||||
'optimal_separator_width':(displaywidth - sumlength)/(ncols-1) if (ncols -1) else 0, | ||||
'rows_numbers' : nrow, | ||||
'columns_width' : chk | ||||
} | ||||
Fernando Perez
|
r7719 | |||
Matthias BUSSONNIER
|
r7387 | 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] | ||||
Fernando Perez
|
r7719 | |||
Matthias BUSSONNIER
|
r7388 | @skip_doctest | ||
Matthias BUSSONNIER
|
r7393 | def compute_item_matrix(items, empty=None, *args, **kwargs) : | ||
Matthias BUSSONNIER
|
r7388 | """Returns a nested list, and info to columnize items | ||
Thomas Kluyver
|
r9244 | Parameters | ||
---------- | ||||
Matthias BUSSONNIER
|
r7388 | |||
items : | ||||
list of strings to columize | ||||
Matthias BUSSONNIER
|
r7393 | empty : (default None) | ||
default value to fill list if needed | ||||
Matthias BUSSONNIER
|
r7388 | 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 | ||||
Thomas Kluyver
|
r9244 | Returns | ||
------- | ||||
Matthias BUSSONNIER
|
r7387 | |||
Returns a tuple of (strings_matrix, dict_info) | ||||
Matthias BUSSONNIER
|
r7388 | 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 | ||||
Thomas Kluyver
|
r9244 | Examples | ||
-------- | ||||
Matthias BUSSONNIER
|
r7388 | |||
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}) | ||||
Matthias BUSSONNIER
|
r7387 | """ | ||
info = _find_optimal(map(len, items), *args, **kwargs) | ||||
nrow, ncol = info['rows_numbers'], info['columns_numbers'] | ||||
Matthias BUSSONNIER
|
r7392 | return ([[ _get_or_default(items, c*nrow+i, default=empty) for c in range(ncol) ] for i in range(nrow) ], info) | ||
MinRK
|
r4004 | |||
Fernando Perez
|
r7719 | |||
Fernando Perez
|
r4537 | 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. | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r4537 | Returns | ||
------- | ||||
The formatted string. | ||||
""" | ||||
Matthias BUSSONNIER
|
r7387 | if not items : | ||
Fernando Perez
|
r4537 | return '\n' | ||
Matthias BUSSONNIER
|
r7388 | matrix, info = compute_item_matrix(items, separator_size=len(separator), displaywidth=displaywidth) | ||
Matthias BUSSONNIER
|
r7387 | 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' | ||||