##// END OF EJS Templates
Copy EvalFormatter and FullEvalFormatter from prompt-manager branch.
Thomas Kluyver -
Show More
@@ -1,739 +1,753 b''
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for working with strings and text.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2009 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import __main__
18 18
19 19 import locale
20 20 import os
21 21 import re
22 22 import shutil
23 23 import sys
24 24 import textwrap
25 25 from string import Formatter
26 26
27 27 from IPython.external.path import path
28 28 from IPython.utils import py3compat
29 29 from IPython.utils.io import nlprint
30 30 from IPython.utils.data import flatten
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Code
34 34 #-----------------------------------------------------------------------------
35 35
36 36 # Less conservative replacement for sys.getdefaultencoding, that will try
37 37 # to match the environment.
38 38 # Defined here as central function, so if we find better choices, we
39 39 # won't need to make changes all over IPython.
40 40 def getdefaultencoding():
41 41 """Return IPython's guess for the default encoding for bytes as text.
42 42
43 43 Asks for stdin.encoding first, to match the calling Terminal, but that
44 44 is often None for subprocesses. Fall back on locale.getpreferredencoding()
45 45 which should be a sensible platform default (that respects LANG environment),
46 46 and finally to sys.getdefaultencoding() which is the most conservative option,
47 47 and usually ASCII.
48 48 """
49 49 enc = sys.stdin.encoding
50 50 if not enc or enc=='ascii':
51 51 try:
52 52 # There are reports of getpreferredencoding raising errors
53 53 # in some cases, which may well be fixed, but let's be conservative here.
54 54 enc = locale.getpreferredencoding()
55 55 except Exception:
56 56 pass
57 57 return enc or sys.getdefaultencoding()
58 58
59 59 def unquote_ends(istr):
60 60 """Remove a single pair of quotes from the endpoints of a string."""
61 61
62 62 if not istr:
63 63 return istr
64 64 if (istr[0]=="'" and istr[-1]=="'") or \
65 65 (istr[0]=='"' and istr[-1]=='"'):
66 66 return istr[1:-1]
67 67 else:
68 68 return istr
69 69
70 70
71 71 class LSString(str):
72 72 """String derivative with a special access attributes.
73 73
74 74 These are normal strings, but with the special attributes:
75 75
76 76 .l (or .list) : value as list (split on newlines).
77 77 .n (or .nlstr): original value (the string itself).
78 78 .s (or .spstr): value as whitespace-separated string.
79 79 .p (or .paths): list of path objects
80 80
81 81 Any values which require transformations are computed only once and
82 82 cached.
83 83
84 84 Such strings are very useful to efficiently interact with the shell, which
85 85 typically only understands whitespace-separated options for commands."""
86 86
87 87 def get_list(self):
88 88 try:
89 89 return self.__list
90 90 except AttributeError:
91 91 self.__list = self.split('\n')
92 92 return self.__list
93 93
94 94 l = list = property(get_list)
95 95
96 96 def get_spstr(self):
97 97 try:
98 98 return self.__spstr
99 99 except AttributeError:
100 100 self.__spstr = self.replace('\n',' ')
101 101 return self.__spstr
102 102
103 103 s = spstr = property(get_spstr)
104 104
105 105 def get_nlstr(self):
106 106 return self
107 107
108 108 n = nlstr = property(get_nlstr)
109 109
110 110 def get_paths(self):
111 111 try:
112 112 return self.__paths
113 113 except AttributeError:
114 114 self.__paths = [path(p) for p in self.split('\n') if os.path.exists(p)]
115 115 return self.__paths
116 116
117 117 p = paths = property(get_paths)
118 118
119 119 # FIXME: We need to reimplement type specific displayhook and then add this
120 120 # back as a custom printer. This should also be moved outside utils into the
121 121 # core.
122 122
123 123 # def print_lsstring(arg):
124 124 # """ Prettier (non-repr-like) and more informative printer for LSString """
125 125 # print "LSString (.p, .n, .l, .s available). Value:"
126 126 # print arg
127 127 #
128 128 #
129 129 # print_lsstring = result_display.when_type(LSString)(print_lsstring)
130 130
131 131
132 132 class SList(list):
133 133 """List derivative with a special access attributes.
134 134
135 135 These are normal lists, but with the special attributes:
136 136
137 137 .l (or .list) : value as list (the list itself).
138 138 .n (or .nlstr): value as a string, joined on newlines.
139 139 .s (or .spstr): value as a string, joined on spaces.
140 140 .p (or .paths): list of path objects
141 141
142 142 Any values which require transformations are computed only once and
143 143 cached."""
144 144
145 145 def get_list(self):
146 146 return self
147 147
148 148 l = list = property(get_list)
149 149
150 150 def get_spstr(self):
151 151 try:
152 152 return self.__spstr
153 153 except AttributeError:
154 154 self.__spstr = ' '.join(self)
155 155 return self.__spstr
156 156
157 157 s = spstr = property(get_spstr)
158 158
159 159 def get_nlstr(self):
160 160 try:
161 161 return self.__nlstr
162 162 except AttributeError:
163 163 self.__nlstr = '\n'.join(self)
164 164 return self.__nlstr
165 165
166 166 n = nlstr = property(get_nlstr)
167 167
168 168 def get_paths(self):
169 169 try:
170 170 return self.__paths
171 171 except AttributeError:
172 172 self.__paths = [path(p) for p in self if os.path.exists(p)]
173 173 return self.__paths
174 174
175 175 p = paths = property(get_paths)
176 176
177 177 def grep(self, pattern, prune = False, field = None):
178 178 """ Return all strings matching 'pattern' (a regex or callable)
179 179
180 180 This is case-insensitive. If prune is true, return all items
181 181 NOT matching the pattern.
182 182
183 183 If field is specified, the match must occur in the specified
184 184 whitespace-separated field.
185 185
186 186 Examples::
187 187
188 188 a.grep( lambda x: x.startswith('C') )
189 189 a.grep('Cha.*log', prune=1)
190 190 a.grep('chm', field=-1)
191 191 """
192 192
193 193 def match_target(s):
194 194 if field is None:
195 195 return s
196 196 parts = s.split()
197 197 try:
198 198 tgt = parts[field]
199 199 return tgt
200 200 except IndexError:
201 201 return ""
202 202
203 203 if isinstance(pattern, basestring):
204 204 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
205 205 else:
206 206 pred = pattern
207 207 if not prune:
208 208 return SList([el for el in self if pred(match_target(el))])
209 209 else:
210 210 return SList([el for el in self if not pred(match_target(el))])
211 211
212 212 def fields(self, *fields):
213 213 """ Collect whitespace-separated fields from string list
214 214
215 215 Allows quick awk-like usage of string lists.
216 216
217 217 Example data (in var a, created by 'a = !ls -l')::
218 218 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
219 219 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
220 220
221 221 a.fields(0) is ['-rwxrwxrwx', 'drwxrwxrwx+']
222 222 a.fields(1,0) is ['1 -rwxrwxrwx', '6 drwxrwxrwx+']
223 223 (note the joining by space).
224 224 a.fields(-1) is ['ChangeLog', 'IPython']
225 225
226 226 IndexErrors are ignored.
227 227
228 228 Without args, fields() just split()'s the strings.
229 229 """
230 230 if len(fields) == 0:
231 231 return [el.split() for el in self]
232 232
233 233 res = SList()
234 234 for el in [f.split() for f in self]:
235 235 lineparts = []
236 236
237 237 for fd in fields:
238 238 try:
239 239 lineparts.append(el[fd])
240 240 except IndexError:
241 241 pass
242 242 if lineparts:
243 243 res.append(" ".join(lineparts))
244 244
245 245 return res
246 246
247 247 def sort(self,field= None, nums = False):
248 248 """ sort by specified fields (see fields())
249 249
250 250 Example::
251 251 a.sort(1, nums = True)
252 252
253 253 Sorts a by second field, in numerical order (so that 21 > 3)
254 254
255 255 """
256 256
257 257 #decorate, sort, undecorate
258 258 if field is not None:
259 259 dsu = [[SList([line]).fields(field), line] for line in self]
260 260 else:
261 261 dsu = [[line, line] for line in self]
262 262 if nums:
263 263 for i in range(len(dsu)):
264 264 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
265 265 try:
266 266 n = int(numstr)
267 267 except ValueError:
268 268 n = 0;
269 269 dsu[i][0] = n
270 270
271 271
272 272 dsu.sort()
273 273 return SList([t[1] for t in dsu])
274 274
275 275
276 276 # FIXME: We need to reimplement type specific displayhook and then add this
277 277 # back as a custom printer. This should also be moved outside utils into the
278 278 # core.
279 279
280 280 # def print_slist(arg):
281 281 # """ Prettier (non-repr-like) and more informative printer for SList """
282 282 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
283 283 # if hasattr(arg, 'hideonce') and arg.hideonce:
284 284 # arg.hideonce = False
285 285 # return
286 286 #
287 287 # nlprint(arg)
288 288 #
289 289 # print_slist = result_display.when_type(SList)(print_slist)
290 290
291 291
292 292 def esc_quotes(strng):
293 293 """Return the input string with single and double quotes escaped out"""
294 294
295 295 return strng.replace('"','\\"').replace("'","\\'")
296 296
297 297
298 298 def make_quoted_expr(s):
299 299 """Return string s in appropriate quotes, using raw string if possible.
300 300
301 301 XXX - example removed because it caused encoding errors in documentation
302 302 generation. We need a new example that doesn't contain invalid chars.
303 303
304 304 Note the use of raw string and padding at the end to allow trailing
305 305 backslash.
306 306 """
307 307
308 308 tail = ''
309 309 tailpadding = ''
310 310 raw = ''
311 311 ucode = '' if py3compat.PY3 else 'u'
312 312 if "\\" in s:
313 313 raw = 'r'
314 314 if s.endswith('\\'):
315 315 tail = '[:-1]'
316 316 tailpadding = '_'
317 317 if '"' not in s:
318 318 quote = '"'
319 319 elif "'" not in s:
320 320 quote = "'"
321 321 elif '"""' not in s and not s.endswith('"'):
322 322 quote = '"""'
323 323 elif "'''" not in s and not s.endswith("'"):
324 324 quote = "'''"
325 325 else:
326 326 # give up, backslash-escaped string will do
327 327 return '"%s"' % esc_quotes(s)
328 328 res = ucode + raw + quote + s + tailpadding + quote + tail
329 329 return res
330 330
331 331
332 332 def qw(words,flat=0,sep=None,maxsplit=-1):
333 333 """Similar to Perl's qw() operator, but with some more options.
334 334
335 335 qw(words,flat=0,sep=' ',maxsplit=-1) -> words.split(sep,maxsplit)
336 336
337 337 words can also be a list itself, and with flat=1, the output will be
338 338 recursively flattened.
339 339
340 340 Examples:
341 341
342 342 >>> qw('1 2')
343 343 ['1', '2']
344 344
345 345 >>> qw(['a b','1 2',['m n','p q']])
346 346 [['a', 'b'], ['1', '2'], [['m', 'n'], ['p', 'q']]]
347 347
348 348 >>> qw(['a b','1 2',['m n','p q']],flat=1)
349 349 ['a', 'b', '1', '2', 'm', 'n', 'p', 'q']
350 350 """
351 351
352 352 if isinstance(words, basestring):
353 353 return [word.strip() for word in words.split(sep,maxsplit)
354 354 if word and not word.isspace() ]
355 355 if flat:
356 356 return flatten(map(qw,words,[1]*len(words)))
357 357 return map(qw,words)
358 358
359 359
360 360 def qwflat(words,sep=None,maxsplit=-1):
361 361 """Calls qw(words) in flat mode. It's just a convenient shorthand."""
362 362 return qw(words,1,sep,maxsplit)
363 363
364 364
365 365 def qw_lol(indata):
366 366 """qw_lol('a b') -> [['a','b']],
367 367 otherwise it's just a call to qw().
368 368
369 369 We need this to make sure the modules_some keys *always* end up as a
370 370 list of lists."""
371 371
372 372 if isinstance(indata, basestring):
373 373 return [qw(indata)]
374 374 else:
375 375 return qw(indata)
376 376
377 377
378 378 def grep(pat,list,case=1):
379 379 """Simple minded grep-like function.
380 380 grep(pat,list) returns occurrences of pat in list, None on failure.
381 381
382 382 It only does simple string matching, with no support for regexps. Use the
383 383 option case=0 for case-insensitive matching."""
384 384
385 385 # This is pretty crude. At least it should implement copying only references
386 386 # to the original data in case it's big. Now it copies the data for output.
387 387 out=[]
388 388 if case:
389 389 for term in list:
390 390 if term.find(pat)>-1: out.append(term)
391 391 else:
392 392 lpat=pat.lower()
393 393 for term in list:
394 394 if term.lower().find(lpat)>-1: out.append(term)
395 395
396 396 if len(out): return out
397 397 else: return None
398 398
399 399
400 400 def dgrep(pat,*opts):
401 401 """Return grep() on dir()+dir(__builtins__).
402 402
403 403 A very common use of grep() when working interactively."""
404 404
405 405 return grep(pat,dir(__main__)+dir(__main__.__builtins__),*opts)
406 406
407 407
408 408 def idgrep(pat):
409 409 """Case-insensitive dgrep()"""
410 410
411 411 return dgrep(pat,0)
412 412
413 413
414 414 def igrep(pat,list):
415 415 """Synonym for case-insensitive grep."""
416 416
417 417 return grep(pat,list,case=0)
418 418
419 419
420 420 def indent(instr,nspaces=4, ntabs=0, flatten=False):
421 421 """Indent a string a given number of spaces or tabstops.
422 422
423 423 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
424 424
425 425 Parameters
426 426 ----------
427 427
428 428 instr : basestring
429 429 The string to be indented.
430 430 nspaces : int (default: 4)
431 431 The number of spaces to be indented.
432 432 ntabs : int (default: 0)
433 433 The number of tabs to be indented.
434 434 flatten : bool (default: False)
435 435 Whether to scrub existing indentation. If True, all lines will be
436 436 aligned to the same indentation. If False, existing indentation will
437 437 be strictly increased.
438 438
439 439 Returns
440 440 -------
441 441
442 442 str|unicode : string indented by ntabs and nspaces.
443 443
444 444 """
445 445 if instr is None:
446 446 return
447 447 ind = '\t'*ntabs+' '*nspaces
448 448 if flatten:
449 449 pat = re.compile(r'^\s*', re.MULTILINE)
450 450 else:
451 451 pat = re.compile(r'^', re.MULTILINE)
452 452 outstr = re.sub(pat, ind, instr)
453 453 if outstr.endswith(os.linesep+ind):
454 454 return outstr[:-len(ind)]
455 455 else:
456 456 return outstr
457 457
458 458 def native_line_ends(filename,backup=1):
459 459 """Convert (in-place) a file to line-ends native to the current OS.
460 460
461 461 If the optional backup argument is given as false, no backup of the
462 462 original file is left. """
463 463
464 464 backup_suffixes = {'posix':'~','dos':'.bak','nt':'.bak','mac':'.bak'}
465 465
466 466 bak_filename = filename + backup_suffixes[os.name]
467 467
468 468 original = open(filename).read()
469 469 shutil.copy2(filename,bak_filename)
470 470 try:
471 471 new = open(filename,'wb')
472 472 new.write(os.linesep.join(original.splitlines()))
473 473 new.write(os.linesep) # ALWAYS put an eol at the end of the file
474 474 new.close()
475 475 except:
476 476 os.rename(bak_filename,filename)
477 477 if not backup:
478 478 try:
479 479 os.remove(bak_filename)
480 480 except:
481 481 pass
482 482
483 483
484 484 def list_strings(arg):
485 485 """Always return a list of strings, given a string or list of strings
486 486 as input.
487 487
488 488 :Examples:
489 489
490 490 In [7]: list_strings('A single string')
491 491 Out[7]: ['A single string']
492 492
493 493 In [8]: list_strings(['A single string in a list'])
494 494 Out[8]: ['A single string in a list']
495 495
496 496 In [9]: list_strings(['A','list','of','strings'])
497 497 Out[9]: ['A', 'list', 'of', 'strings']
498 498 """
499 499
500 500 if isinstance(arg,basestring): return [arg]
501 501 else: return arg
502 502
503 503
504 504 def marquee(txt='',width=78,mark='*'):
505 505 """Return the input string centered in a 'marquee'.
506 506
507 507 :Examples:
508 508
509 509 In [16]: marquee('A test',40)
510 510 Out[16]: '**************** A test ****************'
511 511
512 512 In [17]: marquee('A test',40,'-')
513 513 Out[17]: '---------------- A test ----------------'
514 514
515 515 In [18]: marquee('A test',40,' ')
516 516 Out[18]: ' A test '
517 517
518 518 """
519 519 if not txt:
520 520 return (mark*width)[:width]
521 521 nmark = (width-len(txt)-2)//len(mark)//2
522 522 if nmark < 0: nmark =0
523 523 marks = mark*nmark
524 524 return '%s %s %s' % (marks,txt,marks)
525 525
526 526
527 527 ini_spaces_re = re.compile(r'^(\s+)')
528 528
529 529 def num_ini_spaces(strng):
530 530 """Return the number of initial spaces in a string"""
531 531
532 532 ini_spaces = ini_spaces_re.match(strng)
533 533 if ini_spaces:
534 534 return ini_spaces.end()
535 535 else:
536 536 return 0
537 537
538 538
539 539 def format_screen(strng):
540 540 """Format a string for screen printing.
541 541
542 542 This removes some latex-type format codes."""
543 543 # Paragraph continue
544 544 par_re = re.compile(r'\\$',re.MULTILINE)
545 545 strng = par_re.sub('',strng)
546 546 return strng
547 547
548 548 def dedent(text):
549 549 """Equivalent of textwrap.dedent that ignores unindented first line.
550 550
551 551 This means it will still dedent strings like:
552 552 '''foo
553 553 is a bar
554 554 '''
555 555
556 556 For use in wrap_paragraphs.
557 557 """
558 558
559 559 if text.startswith('\n'):
560 560 # text starts with blank line, don't ignore the first line
561 561 return textwrap.dedent(text)
562 562
563 563 # split first line
564 564 splits = text.split('\n',1)
565 565 if len(splits) == 1:
566 566 # only one line
567 567 return textwrap.dedent(text)
568 568
569 569 first, rest = splits
570 570 # dedent everything but the first line
571 571 rest = textwrap.dedent(rest)
572 572 return '\n'.join([first, rest])
573 573
574 574 def wrap_paragraphs(text, ncols=80):
575 575 """Wrap multiple paragraphs to fit a specified width.
576 576
577 577 This is equivalent to textwrap.wrap, but with support for multiple
578 578 paragraphs, as separated by empty lines.
579 579
580 580 Returns
581 581 -------
582 582
583 583 list of complete paragraphs, wrapped to fill `ncols` columns.
584 584 """
585 585 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
586 586 text = dedent(text).strip()
587 587 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
588 588 out_ps = []
589 589 indent_re = re.compile(r'\n\s+', re.MULTILINE)
590 590 for p in paragraphs:
591 591 # presume indentation that survives dedent is meaningful formatting,
592 592 # so don't fill unless text is flush.
593 593 if indent_re.search(p) is None:
594 594 # wrap paragraph
595 595 p = textwrap.fill(p, ncols)
596 596 out_ps.append(p)
597 597 return out_ps
598 598
599 599
600
601 600 class EvalFormatter(Formatter):
602 601 """A String Formatter that allows evaluation of simple expressions.
603
604 Any time a format key is not found in the kwargs,
605 it will be tried as an expression in the kwargs namespace.
606
602
603 Note that this version interprets a : as specifying a format string (as per
604 standard string formatting), so if slicing is required, you must explicitly
605 create a slice.
606
607 607 This is to be used in templating cases, such as the parallel batch
608 608 script templates, where simple arithmetic on arguments is useful.
609 609
610 610 Examples
611 611 --------
612
613 In [1]: f = EvalFormatter()
614 In [2]: f.format('{n//4}', n=8)
615 Out [2]: '2'
616
617 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
618 Out [3]: 'll'
619 """
620 def get_field(self, name, args, kwargs):
621 v = eval(name, kwargs)
622 return v, name
612 623
613 In [1]: f = EvalFormatter()
624 class FullEvalFormatter(Formatter):
625 """A String Formatter that allows evaluation of simple expressions.
626
627 Any time a format key is not found in the kwargs,
628 it will be tried as an expression in the kwargs namespace.
629
630 Note that this version allows slicing using [1:2], so you cannot specify
631 a format string. Use :class:`EvalFormatter` to permit format strings.
632
633 Examples
634 --------
635
636 In [1]: f = FullEvalFormatter()
614 637 In [2]: f.format('{n//4}', n=8)
615 638 Out[2]: '2'
616
617 In [3]: f.format('{list(range(3))}')
618 Out[3]: '[0, 1, 2]'
639
640 In [3]: f.format('{list(range(5))[2:4]}')
641 Out[3]: '[2, 3]'
619 642
620 643 In [4]: f.format('{3*2}')
621 644 Out[4]: '6'
622 645 """
623
624 # should we allow slicing by disabling the format_spec feature?
625 allow_slicing = True
626
627 646 # copied from Formatter._vformat with minor changes to allow eval
628 647 # and replace the format_spec code with slicing
629 648 def _vformat(self, format_string, args, kwargs, used_args, recursion_depth):
630 649 if recursion_depth < 0:
631 650 raise ValueError('Max string recursion exceeded')
632 651 result = []
633 652 for literal_text, field_name, format_spec, conversion in \
634 653 self.parse(format_string):
635 654
636 655 # output the literal text
637 656 if literal_text:
638 657 result.append(literal_text)
639 658
640 659 # if there's a field, output it
641 660 if field_name is not None:
642 661 # this is some markup, find the object and do
643 # the formatting
662 # the formatting
644 663
645 if self.allow_slicing and format_spec:
664 if format_spec:
646 665 # override format spec, to allow slicing:
647 666 field_name = ':'.join([field_name, format_spec])
648 format_spec = ''
649 667
650 668 # eval the contents of the field for the object
651 669 # to be formatted
652 670 obj = eval(field_name, kwargs)
653 671
654 672 # do any conversion on the resulting object
655 673 obj = self.convert_field(obj, conversion)
656 674
657 # expand the format spec, if needed
658 format_spec = self._vformat(format_spec, args, kwargs,
659 used_args, recursion_depth-1)
660
661 675 # format the object and append to the result
662 result.append(self.format_field(obj, format_spec))
676 result.append(self.format_field(obj, ''))
663 677
664 678 return ''.join(result)
665 679
666 680
667 681 def columnize(items, separator=' ', displaywidth=80):
668 682 """ Transform a list of strings into a single string with columns.
669 683
670 684 Parameters
671 685 ----------
672 686 items : sequence of strings
673 687 The strings to process.
674 688
675 689 separator : str, optional [default is two spaces]
676 690 The string that separates columns.
677 691
678 692 displaywidth : int, optional [default is 80]
679 693 Width of the display in number of characters.
680 694
681 695 Returns
682 696 -------
683 697 The formatted string.
684 698 """
685 699 # Note: this code is adapted from columnize 0.3.2.
686 700 # See http://code.google.com/p/pycolumnize/
687 701
688 702 # Some degenerate cases.
689 703 size = len(items)
690 704 if size == 0:
691 705 return '\n'
692 706 elif size == 1:
693 707 return '%s\n' % items[0]
694 708
695 709 # Special case: if any item is longer than the maximum width, there's no
696 710 # point in triggering the logic below...
697 711 item_len = map(len, items) # save these, we can reuse them below
698 712 longest = max(item_len)
699 713 if longest >= displaywidth:
700 714 return '\n'.join(items+[''])
701 715
702 716 # Try every row count from 1 upwards
703 717 array_index = lambda nrows, row, col: nrows*col + row
704 718 for nrows in range(1, size):
705 719 ncols = (size + nrows - 1) // nrows
706 720 colwidths = []
707 721 totwidth = -len(separator)
708 722 for col in range(ncols):
709 723 # Get max column width for this column
710 724 colwidth = 0
711 725 for row in range(nrows):
712 726 i = array_index(nrows, row, col)
713 727 if i >= size: break
714 728 x, len_x = items[i], item_len[i]
715 729 colwidth = max(colwidth, len_x)
716 730 colwidths.append(colwidth)
717 731 totwidth += colwidth + len(separator)
718 732 if totwidth > displaywidth:
719 733 break
720 734 if totwidth <= displaywidth:
721 735 break
722 736
723 737 # The smallest number of rows computed and the max widths for each
724 738 # column has been obtained. Now we just have to format each of the rows.
725 739 string = ''
726 740 for row in range(nrows):
727 741 texts = []
728 742 for col in range(ncols):
729 743 i = row + nrows*col
730 744 if i >= size:
731 745 texts.append('')
732 746 else:
733 747 texts.append(items[i])
734 748 while texts and not texts[-1]:
735 749 del texts[-1]
736 750 for col in range(len(texts)):
737 751 texts[col] = texts[col].ljust(colwidths[col])
738 752 string += '%s\n' % separator.join(texts)
739 753 return string
General Comments 0
You need to be logged in to leave comments. Login now