##// END OF EJS Templates
Merge pull request #13041 from MrMino/fix_13039...
Blazej Michalik -
r26620:1dfd4df4 merge
parent child Browse files
Show More
@@ -1,213 +1,200 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Tests for IPython.utils.text"""
2 """Tests for IPython.utils.text"""
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2011 The IPython Development Team
5 # Copyright (C) 2011 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 import os
15 import os
16 import math
16 import math
17 import random
17 import random
18 import sys
18 import sys
19
19
20 import nose.tools as nt
20 import nose.tools as nt
21 from pathlib import Path
21 from pathlib import Path
22
22
23 from IPython.utils import text
23 from IPython.utils import text
24
24
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Globals
26 # Globals
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28
28
29 def test_columnize():
29 def test_columnize():
30 """Basic columnize tests."""
30 """Basic columnize tests."""
31 size = 5
31 size = 5
32 items = [l*size for l in 'abcd']
32 items = [l*size for l in 'abcd']
33
33
34 out = text.columnize(items, displaywidth=80)
34 out = text.columnize(items, displaywidth=80)
35 nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n')
35 nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n')
36 out = text.columnize(items, displaywidth=25)
36 out = text.columnize(items, displaywidth=25)
37 nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n')
37 nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n')
38 out = text.columnize(items, displaywidth=12)
38 out = text.columnize(items, displaywidth=12)
39 nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n')
39 nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n')
40 out = text.columnize(items, displaywidth=10)
40 out = text.columnize(items, displaywidth=10)
41 nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n')
41 nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n')
42
42
43 out = text.columnize(items, row_first=True, displaywidth=80)
43 out = text.columnize(items, row_first=True, displaywidth=80)
44 nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n')
44 nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n')
45 out = text.columnize(items, row_first=True, displaywidth=25)
45 out = text.columnize(items, row_first=True, displaywidth=25)
46 nt.assert_equal(out, 'aaaaa bbbbb\nccccc ddddd\n')
46 nt.assert_equal(out, 'aaaaa bbbbb\nccccc ddddd\n')
47 out = text.columnize(items, row_first=True, displaywidth=12)
47 out = text.columnize(items, row_first=True, displaywidth=12)
48 nt.assert_equal(out, 'aaaaa bbbbb\nccccc ddddd\n')
48 nt.assert_equal(out, 'aaaaa bbbbb\nccccc ddddd\n')
49 out = text.columnize(items, row_first=True, displaywidth=10)
49 out = text.columnize(items, row_first=True, displaywidth=10)
50 nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n')
50 nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n')
51
51
52 out = text.columnize(items, displaywidth=40, spread=True)
52 out = text.columnize(items, displaywidth=40, spread=True)
53 nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n')
53 nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n')
54 out = text.columnize(items, displaywidth=20, spread=True)
54 out = text.columnize(items, displaywidth=20, spread=True)
55 nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n')
55 nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n')
56 out = text.columnize(items, displaywidth=12, spread=True)
56 out = text.columnize(items, displaywidth=12, spread=True)
57 nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n')
57 nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n')
58 out = text.columnize(items, displaywidth=10, spread=True)
58 out = text.columnize(items, displaywidth=10, spread=True)
59 nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n')
59 nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n')
60
60
61
61
62 def test_columnize_random():
62 def test_columnize_random():
63 """Test with random input to hopefully catch edge case """
63 """Test with random input to hopefully catch edge case """
64 for row_first in [True, False]:
64 for row_first in [True, False]:
65 for nitems in [random.randint(2,70) for i in range(2,20)]:
65 for nitems in [random.randint(2,70) for i in range(2,20)]:
66 displaywidth = random.randint(20,200)
66 displaywidth = random.randint(20,200)
67 rand_len = [random.randint(2,displaywidth) for i in range(nitems)]
67 rand_len = [random.randint(2,displaywidth) for i in range(nitems)]
68 items = ['x'*l for l in rand_len]
68 items = ['x'*l for l in rand_len]
69 out = text.columnize(items, row_first=row_first, displaywidth=displaywidth)
69 out = text.columnize(items, row_first=row_first, displaywidth=displaywidth)
70 longer_line = max([len(x) for x in out.split('\n')])
70 longer_line = max([len(x) for x in out.split('\n')])
71 longer_element = max(rand_len)
71 longer_element = max(rand_len)
72 if longer_line > displaywidth:
72 if longer_line > displaywidth:
73 print("Columnize displayed something lager than displaywidth : %s " % longer_line)
73 print("Columnize displayed something lager than displaywidth : %s " % longer_line)
74 print("longer element : %s " % longer_element)
74 print("longer element : %s " % longer_element)
75 print("displaywidth : %s " % displaywidth)
75 print("displaywidth : %s " % displaywidth)
76 print("number of element : %s " % nitems)
76 print("number of element : %s " % nitems)
77 print("size of each element :\n %s" % rand_len)
77 print("size of each element :\n %s" % rand_len)
78 assert False, "row_first={0}".format(row_first)
78 assert False, "row_first={0}".format(row_first)
79
79
80 def test_columnize_medium():
80 def test_columnize_medium():
81 """Test with inputs than shouldn't be wider than 80"""
81 """Test with inputs than shouldn't be wider than 80"""
82 size = 40
82 size = 40
83 items = [l*size for l in 'abc']
83 items = [l*size for l in 'abc']
84 for row_first in [True, False]:
84 for row_first in [True, False]:
85 out = text.columnize(items, row_first=row_first, displaywidth=80)
85 out = text.columnize(items, row_first=row_first, displaywidth=80)
86 nt.assert_equal(out, '\n'.join(items+['']), "row_first={0}".format(row_first))
86 nt.assert_equal(out, '\n'.join(items+['']), "row_first={0}".format(row_first))
87
87
88 def test_columnize_long():
88 def test_columnize_long():
89 """Test columnize with inputs longer than the display window"""
89 """Test columnize with inputs longer than the display window"""
90 size = 11
90 size = 11
91 items = [l*size for l in 'abc']
91 items = [l*size for l in 'abc']
92 for row_first in [True, False]:
92 for row_first in [True, False]:
93 out = text.columnize(items, row_first=row_first, displaywidth=size-1)
93 out = text.columnize(items, row_first=row_first, displaywidth=size-1)
94 nt.assert_equal(out, '\n'.join(items+['']), "row_first={0}".format(row_first))
94 nt.assert_equal(out, '\n'.join(items+['']), "row_first={0}".format(row_first))
95
95
96 def eval_formatter_check(f):
96 def eval_formatter_check(f):
97 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os, u=u"cafΓ©", b="cafΓ©")
97 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os, u=u"cafΓ©", b="cafΓ©")
98 s = f.format("{n} {n//4} {stuff.split()[0]}", **ns)
98 s = f.format("{n} {n//4} {stuff.split()[0]}", **ns)
99 nt.assert_equal(s, "12 3 hello")
99 nt.assert_equal(s, "12 3 hello")
100 s = f.format(' '.join(['{n//%i}'%i for i in range(1,8)]), **ns)
100 s = f.format(' '.join(['{n//%i}'%i for i in range(1,8)]), **ns)
101 nt.assert_equal(s, "12 6 4 3 2 2 1")
101 nt.assert_equal(s, "12 6 4 3 2 2 1")
102 s = f.format('{[n//i for i in range(1,8)]}', **ns)
102 s = f.format('{[n//i for i in range(1,8)]}', **ns)
103 nt.assert_equal(s, "[12, 6, 4, 3, 2, 2, 1]")
103 nt.assert_equal(s, "[12, 6, 4, 3, 2, 2, 1]")
104 s = f.format("{stuff!s}", **ns)
104 s = f.format("{stuff!s}", **ns)
105 nt.assert_equal(s, ns['stuff'])
105 nt.assert_equal(s, ns['stuff'])
106 s = f.format("{stuff!r}", **ns)
106 s = f.format("{stuff!r}", **ns)
107 nt.assert_equal(s, repr(ns['stuff']))
107 nt.assert_equal(s, repr(ns['stuff']))
108
108
109 # Check with unicode:
109 # Check with unicode:
110 s = f.format("{u}", **ns)
110 s = f.format("{u}", **ns)
111 nt.assert_equal(s, ns['u'])
111 nt.assert_equal(s, ns['u'])
112 # This decodes in a platform dependent manner, but it shouldn't error out
112 # This decodes in a platform dependent manner, but it shouldn't error out
113 s = f.format("{b}", **ns)
113 s = f.format("{b}", **ns)
114
114
115 nt.assert_raises(NameError, f.format, '{dne}', **ns)
115 nt.assert_raises(NameError, f.format, '{dne}', **ns)
116
116
117 def eval_formatter_slicing_check(f):
117 def eval_formatter_slicing_check(f):
118 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
118 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
119 s = f.format(" {stuff.split()[:]} ", **ns)
119 s = f.format(" {stuff.split()[:]} ", **ns)
120 nt.assert_equal(s, " ['hello', 'there'] ")
120 nt.assert_equal(s, " ['hello', 'there'] ")
121 s = f.format(" {stuff.split()[::-1]} ", **ns)
121 s = f.format(" {stuff.split()[::-1]} ", **ns)
122 nt.assert_equal(s, " ['there', 'hello'] ")
122 nt.assert_equal(s, " ['there', 'hello'] ")
123 s = f.format("{stuff[::2]}", **ns)
123 s = f.format("{stuff[::2]}", **ns)
124 nt.assert_equal(s, ns['stuff'][::2])
124 nt.assert_equal(s, ns['stuff'][::2])
125
125
126 nt.assert_raises(SyntaxError, f.format, "{n:x}", **ns)
126 nt.assert_raises(SyntaxError, f.format, "{n:x}", **ns)
127
127
128 def eval_formatter_no_slicing_check(f):
128 def eval_formatter_no_slicing_check(f):
129 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
129 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
130
130
131 s = f.format('{n:x} {pi**2:+f}', **ns)
131 s = f.format('{n:x} {pi**2:+f}', **ns)
132 nt.assert_equal(s, "c +9.869604")
132 nt.assert_equal(s, "c +9.869604")
133
133
134 s = f.format('{stuff[slice(1,4)]}', **ns)
134 s = f.format('{stuff[slice(1,4)]}', **ns)
135 nt.assert_equal(s, 'ell')
135 nt.assert_equal(s, 'ell')
136
136
137 s = f.format("{a[:]}", a=[1, 2])
137 s = f.format("{a[:]}", a=[1, 2])
138 nt.assert_equal(s, "[1, 2]")
138 nt.assert_equal(s, "[1, 2]")
139
139
140 def test_eval_formatter():
140 def test_eval_formatter():
141 f = text.EvalFormatter()
141 f = text.EvalFormatter()
142 eval_formatter_check(f)
142 eval_formatter_check(f)
143 eval_formatter_no_slicing_check(f)
143 eval_formatter_no_slicing_check(f)
144
144
145 def test_full_eval_formatter():
145 def test_full_eval_formatter():
146 f = text.FullEvalFormatter()
146 f = text.FullEvalFormatter()
147 eval_formatter_check(f)
147 eval_formatter_check(f)
148 eval_formatter_slicing_check(f)
148 eval_formatter_slicing_check(f)
149
149
150 def test_dollar_formatter():
150 def test_dollar_formatter():
151 f = text.DollarFormatter()
151 f = text.DollarFormatter()
152 eval_formatter_check(f)
152 eval_formatter_check(f)
153 eval_formatter_slicing_check(f)
153 eval_formatter_slicing_check(f)
154
154
155 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
155 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
156 s = f.format("$n", **ns)
156 s = f.format("$n", **ns)
157 nt.assert_equal(s, "12")
157 nt.assert_equal(s, "12")
158 s = f.format("$n.real", **ns)
158 s = f.format("$n.real", **ns)
159 nt.assert_equal(s, "12")
159 nt.assert_equal(s, "12")
160 s = f.format("$n/{stuff[:5]}", **ns)
160 s = f.format("$n/{stuff[:5]}", **ns)
161 nt.assert_equal(s, "12/hello")
161 nt.assert_equal(s, "12/hello")
162 s = f.format("$n $$HOME", **ns)
162 s = f.format("$n $$HOME", **ns)
163 nt.assert_equal(s, "12 $HOME")
163 nt.assert_equal(s, "12 $HOME")
164 s = f.format("${foo}", foo="HOME")
164 s = f.format("${foo}", foo="HOME")
165 nt.assert_equal(s, "$HOME")
165 nt.assert_equal(s, "$HOME")
166
166
167
167
168 def test_long_substr():
169 data = ['hi']
170 nt.assert_equal(text.long_substr(data), 'hi')
171
172
173 def test_long_substr2():
174 data = ['abc', 'abd', 'abf', 'ab']
175 nt.assert_equal(text.long_substr(data), 'ab')
176
177 def test_long_substr_empty():
178 data = []
179 nt.assert_equal(text.long_substr(data), '')
180
181 def test_strip_email():
168 def test_strip_email():
182 src = """\
169 src = """\
183 >> >>> def f(x):
170 >> >>> def f(x):
184 >> ... return x+1
171 >> ... return x+1
185 >> ...
172 >> ...
186 >> >>> zz = f(2.5)"""
173 >> >>> zz = f(2.5)"""
187 cln = """\
174 cln = """\
188 >>> def f(x):
175 >>> def f(x):
189 ... return x+1
176 ... return x+1
190 ...
177 ...
191 >>> zz = f(2.5)"""
178 >>> zz = f(2.5)"""
192 nt.assert_equal(text.strip_email_quotes(src), cln)
179 nt.assert_equal(text.strip_email_quotes(src), cln)
193
180
194
181
195 def test_strip_email2():
182 def test_strip_email2():
196 src = '> > > list()'
183 src = '> > > list()'
197 cln = 'list()'
184 cln = 'list()'
198 nt.assert_equal(text.strip_email_quotes(src), cln)
185 nt.assert_equal(text.strip_email_quotes(src), cln)
199
186
200 def test_LSString():
187 def test_LSString():
201 lss = text.LSString("abc\ndef")
188 lss = text.LSString("abc\ndef")
202 nt.assert_equal(lss.l, ['abc', 'def'])
189 nt.assert_equal(lss.l, ['abc', 'def'])
203 nt.assert_equal(lss.s, 'abc def')
190 nt.assert_equal(lss.s, 'abc def')
204 lss = text.LSString(os.getcwd())
191 lss = text.LSString(os.getcwd())
205 nt.assert_is_instance(lss.p[0], Path)
192 nt.assert_is_instance(lss.p[0], Path)
206
193
207 def test_SList():
194 def test_SList():
208 sl = text.SList(['a 11', 'b 1', 'a 2'])
195 sl = text.SList(['a 11', 'b 1', 'a 2'])
209 nt.assert_equal(sl.n, 'a 11\nb 1\na 2')
196 nt.assert_equal(sl.n, 'a 11\nb 1\na 2')
210 nt.assert_equal(sl.s, 'a 11 b 1 a 2')
197 nt.assert_equal(sl.s, 'a 11 b 1 a 2')
211 nt.assert_equal(sl.grep(lambda x: x.startswith('a')), text.SList(['a 11', 'a 2']))
198 nt.assert_equal(sl.grep(lambda x: x.startswith('a')), text.SList(['a 11', 'a 2']))
212 nt.assert_equal(sl.fields(0), text.SList(['a', 'b', 'a']))
199 nt.assert_equal(sl.fields(0), text.SList(['a', 'b', 'a']))
213 nt.assert_equal(sl.sort(field=1, nums=True), text.SList(['b 1', 'a 2', 'a 11']))
200 nt.assert_equal(sl.sort(field=1, nums=True), text.SList(['b 1', 'a 2', 'a 11']))
@@ -1,761 +1,749 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Utilities for working with strings and text.
3 Utilities for working with strings and text.
4
4
5 Inheritance diagram:
5 Inheritance diagram:
6
6
7 .. inheritance-diagram:: IPython.utils.text
7 .. inheritance-diagram:: IPython.utils.text
8 :parts: 3
8 :parts: 3
9 """
9 """
10
10
11 import os
11 import os
12 import re
12 import re
13 import string
13 import sys
14 import sys
14 import textwrap
15 import textwrap
15 from string import Formatter
16 from string import Formatter
16 from pathlib import Path
17 from pathlib import Path
17
18
18
19
19 # datetime.strftime date format for ipython
20 # datetime.strftime date format for ipython
20 if sys.platform == 'win32':
21 if sys.platform == 'win32':
21 date_format = "%B %d, %Y"
22 date_format = "%B %d, %Y"
22 else:
23 else:
23 date_format = "%B %-d, %Y"
24 date_format = "%B %-d, %Y"
24
25
25 class LSString(str):
26 class LSString(str):
26 """String derivative with a special access attributes.
27 """String derivative with a special access attributes.
27
28
28 These are normal strings, but with the special attributes:
29 These are normal strings, but with the special attributes:
29
30
30 .l (or .list) : value as list (split on newlines).
31 .l (or .list) : value as list (split on newlines).
31 .n (or .nlstr): original value (the string itself).
32 .n (or .nlstr): original value (the string itself).
32 .s (or .spstr): value as whitespace-separated string.
33 .s (or .spstr): value as whitespace-separated string.
33 .p (or .paths): list of path objects (requires path.py package)
34 .p (or .paths): list of path objects (requires path.py package)
34
35
35 Any values which require transformations are computed only once and
36 Any values which require transformations are computed only once and
36 cached.
37 cached.
37
38
38 Such strings are very useful to efficiently interact with the shell, which
39 Such strings are very useful to efficiently interact with the shell, which
39 typically only understands whitespace-separated options for commands."""
40 typically only understands whitespace-separated options for commands."""
40
41
41 def get_list(self):
42 def get_list(self):
42 try:
43 try:
43 return self.__list
44 return self.__list
44 except AttributeError:
45 except AttributeError:
45 self.__list = self.split('\n')
46 self.__list = self.split('\n')
46 return self.__list
47 return self.__list
47
48
48 l = list = property(get_list)
49 l = list = property(get_list)
49
50
50 def get_spstr(self):
51 def get_spstr(self):
51 try:
52 try:
52 return self.__spstr
53 return self.__spstr
53 except AttributeError:
54 except AttributeError:
54 self.__spstr = self.replace('\n',' ')
55 self.__spstr = self.replace('\n',' ')
55 return self.__spstr
56 return self.__spstr
56
57
57 s = spstr = property(get_spstr)
58 s = spstr = property(get_spstr)
58
59
59 def get_nlstr(self):
60 def get_nlstr(self):
60 return self
61 return self
61
62
62 n = nlstr = property(get_nlstr)
63 n = nlstr = property(get_nlstr)
63
64
64 def get_paths(self):
65 def get_paths(self):
65 try:
66 try:
66 return self.__paths
67 return self.__paths
67 except AttributeError:
68 except AttributeError:
68 self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)]
69 self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)]
69 return self.__paths
70 return self.__paths
70
71
71 p = paths = property(get_paths)
72 p = paths = property(get_paths)
72
73
73 # FIXME: We need to reimplement type specific displayhook and then add this
74 # FIXME: We need to reimplement type specific displayhook and then add this
74 # back as a custom printer. This should also be moved outside utils into the
75 # back as a custom printer. This should also be moved outside utils into the
75 # core.
76 # core.
76
77
77 # def print_lsstring(arg):
78 # def print_lsstring(arg):
78 # """ Prettier (non-repr-like) and more informative printer for LSString """
79 # """ Prettier (non-repr-like) and more informative printer for LSString """
79 # print "LSString (.p, .n, .l, .s available). Value:"
80 # print "LSString (.p, .n, .l, .s available). Value:"
80 # print arg
81 # print arg
81 #
82 #
82 #
83 #
83 # print_lsstring = result_display.register(LSString)(print_lsstring)
84 # print_lsstring = result_display.register(LSString)(print_lsstring)
84
85
85
86
86 class SList(list):
87 class SList(list):
87 """List derivative with a special access attributes.
88 """List derivative with a special access attributes.
88
89
89 These are normal lists, but with the special attributes:
90 These are normal lists, but with the special attributes:
90
91
91 * .l (or .list) : value as list (the list itself).
92 * .l (or .list) : value as list (the list itself).
92 * .n (or .nlstr): value as a string, joined on newlines.
93 * .n (or .nlstr): value as a string, joined on newlines.
93 * .s (or .spstr): value as a string, joined on spaces.
94 * .s (or .spstr): value as a string, joined on spaces.
94 * .p (or .paths): list of path objects (requires path.py package)
95 * .p (or .paths): list of path objects (requires path.py package)
95
96
96 Any values which require transformations are computed only once and
97 Any values which require transformations are computed only once and
97 cached."""
98 cached."""
98
99
99 def get_list(self):
100 def get_list(self):
100 return self
101 return self
101
102
102 l = list = property(get_list)
103 l = list = property(get_list)
103
104
104 def get_spstr(self):
105 def get_spstr(self):
105 try:
106 try:
106 return self.__spstr
107 return self.__spstr
107 except AttributeError:
108 except AttributeError:
108 self.__spstr = ' '.join(self)
109 self.__spstr = ' '.join(self)
109 return self.__spstr
110 return self.__spstr
110
111
111 s = spstr = property(get_spstr)
112 s = spstr = property(get_spstr)
112
113
113 def get_nlstr(self):
114 def get_nlstr(self):
114 try:
115 try:
115 return self.__nlstr
116 return self.__nlstr
116 except AttributeError:
117 except AttributeError:
117 self.__nlstr = '\n'.join(self)
118 self.__nlstr = '\n'.join(self)
118 return self.__nlstr
119 return self.__nlstr
119
120
120 n = nlstr = property(get_nlstr)
121 n = nlstr = property(get_nlstr)
121
122
122 def get_paths(self):
123 def get_paths(self):
123 try:
124 try:
124 return self.__paths
125 return self.__paths
125 except AttributeError:
126 except AttributeError:
126 self.__paths = [Path(p) for p in self if os.path.exists(p)]
127 self.__paths = [Path(p) for p in self if os.path.exists(p)]
127 return self.__paths
128 return self.__paths
128
129
129 p = paths = property(get_paths)
130 p = paths = property(get_paths)
130
131
131 def grep(self, pattern, prune = False, field = None):
132 def grep(self, pattern, prune = False, field = None):
132 """ Return all strings matching 'pattern' (a regex or callable)
133 """ Return all strings matching 'pattern' (a regex or callable)
133
134
134 This is case-insensitive. If prune is true, return all items
135 This is case-insensitive. If prune is true, return all items
135 NOT matching the pattern.
136 NOT matching the pattern.
136
137
137 If field is specified, the match must occur in the specified
138 If field is specified, the match must occur in the specified
138 whitespace-separated field.
139 whitespace-separated field.
139
140
140 Examples::
141 Examples::
141
142
142 a.grep( lambda x: x.startswith('C') )
143 a.grep( lambda x: x.startswith('C') )
143 a.grep('Cha.*log', prune=1)
144 a.grep('Cha.*log', prune=1)
144 a.grep('chm', field=-1)
145 a.grep('chm', field=-1)
145 """
146 """
146
147
147 def match_target(s):
148 def match_target(s):
148 if field is None:
149 if field is None:
149 return s
150 return s
150 parts = s.split()
151 parts = s.split()
151 try:
152 try:
152 tgt = parts[field]
153 tgt = parts[field]
153 return tgt
154 return tgt
154 except IndexError:
155 except IndexError:
155 return ""
156 return ""
156
157
157 if isinstance(pattern, str):
158 if isinstance(pattern, str):
158 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
159 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
159 else:
160 else:
160 pred = pattern
161 pred = pattern
161 if not prune:
162 if not prune:
162 return SList([el for el in self if pred(match_target(el))])
163 return SList([el for el in self if pred(match_target(el))])
163 else:
164 else:
164 return SList([el for el in self if not pred(match_target(el))])
165 return SList([el for el in self if not pred(match_target(el))])
165
166
166 def fields(self, *fields):
167 def fields(self, *fields):
167 """ Collect whitespace-separated fields from string list
168 """ Collect whitespace-separated fields from string list
168
169
169 Allows quick awk-like usage of string lists.
170 Allows quick awk-like usage of string lists.
170
171
171 Example data (in var a, created by 'a = !ls -l')::
172 Example data (in var a, created by 'a = !ls -l')::
172
173
173 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
174 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
174 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
175 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
175
176
176 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
177 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
177 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
178 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
178 (note the joining by space).
179 (note the joining by space).
179 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
180 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
180
181
181 IndexErrors are ignored.
182 IndexErrors are ignored.
182
183
183 Without args, fields() just split()'s the strings.
184 Without args, fields() just split()'s the strings.
184 """
185 """
185 if len(fields) == 0:
186 if len(fields) == 0:
186 return [el.split() for el in self]
187 return [el.split() for el in self]
187
188
188 res = SList()
189 res = SList()
189 for el in [f.split() for f in self]:
190 for el in [f.split() for f in self]:
190 lineparts = []
191 lineparts = []
191
192
192 for fd in fields:
193 for fd in fields:
193 try:
194 try:
194 lineparts.append(el[fd])
195 lineparts.append(el[fd])
195 except IndexError:
196 except IndexError:
196 pass
197 pass
197 if lineparts:
198 if lineparts:
198 res.append(" ".join(lineparts))
199 res.append(" ".join(lineparts))
199
200
200 return res
201 return res
201
202
202 def sort(self,field= None, nums = False):
203 def sort(self,field= None, nums = False):
203 """ sort by specified fields (see fields())
204 """ sort by specified fields (see fields())
204
205
205 Example::
206 Example::
206
207
207 a.sort(1, nums = True)
208 a.sort(1, nums = True)
208
209
209 Sorts a by second field, in numerical order (so that 21 > 3)
210 Sorts a by second field, in numerical order (so that 21 > 3)
210
211
211 """
212 """
212
213
213 #decorate, sort, undecorate
214 #decorate, sort, undecorate
214 if field is not None:
215 if field is not None:
215 dsu = [[SList([line]).fields(field), line] for line in self]
216 dsu = [[SList([line]).fields(field), line] for line in self]
216 else:
217 else:
217 dsu = [[line, line] for line in self]
218 dsu = [[line, line] for line in self]
218 if nums:
219 if nums:
219 for i in range(len(dsu)):
220 for i in range(len(dsu)):
220 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
221 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
221 try:
222 try:
222 n = int(numstr)
223 n = int(numstr)
223 except ValueError:
224 except ValueError:
224 n = 0
225 n = 0
225 dsu[i][0] = n
226 dsu[i][0] = n
226
227
227
228
228 dsu.sort()
229 dsu.sort()
229 return SList([t[1] for t in dsu])
230 return SList([t[1] for t in dsu])
230
231
231
232
232 # FIXME: We need to reimplement type specific displayhook and then add this
233 # FIXME: We need to reimplement type specific displayhook and then add this
233 # back as a custom printer. This should also be moved outside utils into the
234 # back as a custom printer. This should also be moved outside utils into the
234 # core.
235 # core.
235
236
236 # def print_slist(arg):
237 # def print_slist(arg):
237 # """ Prettier (non-repr-like) and more informative printer for SList """
238 # """ Prettier (non-repr-like) and more informative printer for SList """
238 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
239 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
239 # if hasattr(arg, 'hideonce') and arg.hideonce:
240 # if hasattr(arg, 'hideonce') and arg.hideonce:
240 # arg.hideonce = False
241 # arg.hideonce = False
241 # return
242 # return
242 #
243 #
243 # nlprint(arg) # This was a nested list printer, now removed.
244 # nlprint(arg) # This was a nested list printer, now removed.
244 #
245 #
245 # print_slist = result_display.register(SList)(print_slist)
246 # print_slist = result_display.register(SList)(print_slist)
246
247
247
248
248 def indent(instr,nspaces=4, ntabs=0, flatten=False):
249 def indent(instr,nspaces=4, ntabs=0, flatten=False):
249 """Indent a string a given number of spaces or tabstops.
250 """Indent a string a given number of spaces or tabstops.
250
251
251 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
252 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
252
253
253 Parameters
254 Parameters
254 ----------
255 ----------
255 instr : basestring
256 instr : basestring
256 The string to be indented.
257 The string to be indented.
257 nspaces : int (default: 4)
258 nspaces : int (default: 4)
258 The number of spaces to be indented.
259 The number of spaces to be indented.
259 ntabs : int (default: 0)
260 ntabs : int (default: 0)
260 The number of tabs to be indented.
261 The number of tabs to be indented.
261 flatten : bool (default: False)
262 flatten : bool (default: False)
262 Whether to scrub existing indentation. If True, all lines will be
263 Whether to scrub existing indentation. If True, all lines will be
263 aligned to the same indentation. If False, existing indentation will
264 aligned to the same indentation. If False, existing indentation will
264 be strictly increased.
265 be strictly increased.
265
266
266 Returns
267 Returns
267 -------
268 -------
268 str|unicode : string indented by ntabs and nspaces.
269 str|unicode : string indented by ntabs and nspaces.
269
270
270 """
271 """
271 if instr is None:
272 if instr is None:
272 return
273 return
273 ind = '\t'*ntabs+' '*nspaces
274 ind = '\t'*ntabs+' '*nspaces
274 if flatten:
275 if flatten:
275 pat = re.compile(r'^\s*', re.MULTILINE)
276 pat = re.compile(r'^\s*', re.MULTILINE)
276 else:
277 else:
277 pat = re.compile(r'^', re.MULTILINE)
278 pat = re.compile(r'^', re.MULTILINE)
278 outstr = re.sub(pat, ind, instr)
279 outstr = re.sub(pat, ind, instr)
279 if outstr.endswith(os.linesep+ind):
280 if outstr.endswith(os.linesep+ind):
280 return outstr[:-len(ind)]
281 return outstr[:-len(ind)]
281 else:
282 else:
282 return outstr
283 return outstr
283
284
284
285
285 def list_strings(arg):
286 def list_strings(arg):
286 """Always return a list of strings, given a string or list of strings
287 """Always return a list of strings, given a string or list of strings
287 as input.
288 as input.
288
289
289 Examples
290 Examples
290 --------
291 --------
291 ::
292 ::
292
293
293 In [7]: list_strings('A single string')
294 In [7]: list_strings('A single string')
294 Out[7]: ['A single string']
295 Out[7]: ['A single string']
295
296
296 In [8]: list_strings(['A single string in a list'])
297 In [8]: list_strings(['A single string in a list'])
297 Out[8]: ['A single string in a list']
298 Out[8]: ['A single string in a list']
298
299
299 In [9]: list_strings(['A','list','of','strings'])
300 In [9]: list_strings(['A','list','of','strings'])
300 Out[9]: ['A', 'list', 'of', 'strings']
301 Out[9]: ['A', 'list', 'of', 'strings']
301 """
302 """
302
303
303 if isinstance(arg, str):
304 if isinstance(arg, str):
304 return [arg]
305 return [arg]
305 else:
306 else:
306 return arg
307 return arg
307
308
308
309
309 def marquee(txt='',width=78,mark='*'):
310 def marquee(txt='',width=78,mark='*'):
310 """Return the input string centered in a 'marquee'.
311 """Return the input string centered in a 'marquee'.
311
312
312 Examples
313 Examples
313 --------
314 --------
314 ::
315 ::
315
316
316 In [16]: marquee('A test',40)
317 In [16]: marquee('A test',40)
317 Out[16]: '**************** A test ****************'
318 Out[16]: '**************** A test ****************'
318
319
319 In [17]: marquee('A test',40,'-')
320 In [17]: marquee('A test',40,'-')
320 Out[17]: '---------------- A test ----------------'
321 Out[17]: '---------------- A test ----------------'
321
322
322 In [18]: marquee('A test',40,' ')
323 In [18]: marquee('A test',40,' ')
323 Out[18]: ' A test '
324 Out[18]: ' A test '
324
325
325 """
326 """
326 if not txt:
327 if not txt:
327 return (mark*width)[:width]
328 return (mark*width)[:width]
328 nmark = (width-len(txt)-2)//len(mark)//2
329 nmark = (width-len(txt)-2)//len(mark)//2
329 if nmark < 0: nmark =0
330 if nmark < 0: nmark =0
330 marks = mark*nmark
331 marks = mark*nmark
331 return '%s %s %s' % (marks,txt,marks)
332 return '%s %s %s' % (marks,txt,marks)
332
333
333
334
334 ini_spaces_re = re.compile(r'^(\s+)')
335 ini_spaces_re = re.compile(r'^(\s+)')
335
336
336 def num_ini_spaces(strng):
337 def num_ini_spaces(strng):
337 """Return the number of initial spaces in a string"""
338 """Return the number of initial spaces in a string"""
338
339
339 ini_spaces = ini_spaces_re.match(strng)
340 ini_spaces = ini_spaces_re.match(strng)
340 if ini_spaces:
341 if ini_spaces:
341 return ini_spaces.end()
342 return ini_spaces.end()
342 else:
343 else:
343 return 0
344 return 0
344
345
345
346
346 def format_screen(strng):
347 def format_screen(strng):
347 """Format a string for screen printing.
348 """Format a string for screen printing.
348
349
349 This removes some latex-type format codes."""
350 This removes some latex-type format codes."""
350 # Paragraph continue
351 # Paragraph continue
351 par_re = re.compile(r'\\$',re.MULTILINE)
352 par_re = re.compile(r'\\$',re.MULTILINE)
352 strng = par_re.sub('',strng)
353 strng = par_re.sub('',strng)
353 return strng
354 return strng
354
355
355
356
356 def dedent(text):
357 def dedent(text):
357 """Equivalent of textwrap.dedent that ignores unindented first line.
358 """Equivalent of textwrap.dedent that ignores unindented first line.
358
359
359 This means it will still dedent strings like:
360 This means it will still dedent strings like:
360 '''foo
361 '''foo
361 is a bar
362 is a bar
362 '''
363 '''
363
364
364 For use in wrap_paragraphs.
365 For use in wrap_paragraphs.
365 """
366 """
366
367
367 if text.startswith('\n'):
368 if text.startswith('\n'):
368 # text starts with blank line, don't ignore the first line
369 # text starts with blank line, don't ignore the first line
369 return textwrap.dedent(text)
370 return textwrap.dedent(text)
370
371
371 # split first line
372 # split first line
372 splits = text.split('\n',1)
373 splits = text.split('\n',1)
373 if len(splits) == 1:
374 if len(splits) == 1:
374 # only one line
375 # only one line
375 return textwrap.dedent(text)
376 return textwrap.dedent(text)
376
377
377 first, rest = splits
378 first, rest = splits
378 # dedent everything but the first line
379 # dedent everything but the first line
379 rest = textwrap.dedent(rest)
380 rest = textwrap.dedent(rest)
380 return '\n'.join([first, rest])
381 return '\n'.join([first, rest])
381
382
382
383
383 def wrap_paragraphs(text, ncols=80):
384 def wrap_paragraphs(text, ncols=80):
384 """Wrap multiple paragraphs to fit a specified width.
385 """Wrap multiple paragraphs to fit a specified width.
385
386
386 This is equivalent to textwrap.wrap, but with support for multiple
387 This is equivalent to textwrap.wrap, but with support for multiple
387 paragraphs, as separated by empty lines.
388 paragraphs, as separated by empty lines.
388
389
389 Returns
390 Returns
390 -------
391 -------
391 list of complete paragraphs, wrapped to fill `ncols` columns.
392 list of complete paragraphs, wrapped to fill `ncols` columns.
392 """
393 """
393 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
394 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
394 text = dedent(text).strip()
395 text = dedent(text).strip()
395 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
396 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
396 out_ps = []
397 out_ps = []
397 indent_re = re.compile(r'\n\s+', re.MULTILINE)
398 indent_re = re.compile(r'\n\s+', re.MULTILINE)
398 for p in paragraphs:
399 for p in paragraphs:
399 # presume indentation that survives dedent is meaningful formatting,
400 # presume indentation that survives dedent is meaningful formatting,
400 # so don't fill unless text is flush.
401 # so don't fill unless text is flush.
401 if indent_re.search(p) is None:
402 if indent_re.search(p) is None:
402 # wrap paragraph
403 # wrap paragraph
403 p = textwrap.fill(p, ncols)
404 p = textwrap.fill(p, ncols)
404 out_ps.append(p)
405 out_ps.append(p)
405 return out_ps
406 return out_ps
406
407
407
408
408 def long_substr(data):
409 """Return the longest common substring in a list of strings.
410
411 Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python
412 """
413 substr = ''
414 if len(data) > 1 and len(data[0]) > 0:
415 for i in range(len(data[0])):
416 for j in range(len(data[0])-i+1):
417 if j > len(substr) and all(data[0][i:i+j] in x for x in data):
418 substr = data[0][i:i+j]
419 elif len(data) == 1:
420 substr = data[0]
421 return substr
422
423
424 def strip_email_quotes(text):
409 def strip_email_quotes(text):
425 """Strip leading email quotation characters ('>').
410 """Strip leading email quotation characters ('>').
426
411
427 Removes any combination of leading '>' interspersed with whitespace that
412 Removes any combination of leading '>' interspersed with whitespace that
428 appears *identically* in all lines of the input text.
413 appears *identically* in all lines of the input text.
429
414
430 Parameters
415 Parameters
431 ----------
416 ----------
432 text : str
417 text : str
433
418
434 Examples
419 Examples
435 --------
420 --------
436
421
437 Simple uses::
422 Simple uses::
438
423
439 In [2]: strip_email_quotes('> > text')
424 In [2]: strip_email_quotes('> > text')
440 Out[2]: 'text'
425 Out[2]: 'text'
441
426
442 In [3]: strip_email_quotes('> > text\\n> > more')
427 In [3]: strip_email_quotes('> > text\\n> > more')
443 Out[3]: 'text\\nmore'
428 Out[3]: 'text\\nmore'
444
429
445 Note how only the common prefix that appears in all lines is stripped::
430 Note how only the common prefix that appears in all lines is stripped::
446
431
447 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
432 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
448 Out[4]: '> text\\n> more\\nmore...'
433 Out[4]: '> text\\n> more\\nmore...'
449
434
450 So if any line has no quote marks ('>') , then none are stripped from any
435 So if any line has no quote marks ('>'), then none are stripped from any
451 of them ::
436 of them ::
452
437
453 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
438 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
454 Out[5]: '> > text\\n> > more\\nlast different'
439 Out[5]: '> > text\\n> > more\\nlast different'
455 """
440 """
456 lines = text.splitlines()
441 lines = text.splitlines()
457 matches = set()
442 strip_len = 0
458 for line in lines:
443
459 prefix = re.match(r'^(\s*>[ >]*)', line)
444 for characters in zip(*lines):
460 if prefix:
445 # Check if all characters in this position are the same
461 matches.add(prefix.group(1))
446 if len(set(characters)) > 1:
447 break
448 prefix_char = characters[0]
449
450 if prefix_char in string.whitespace or prefix_char == ">":
451 strip_len += 1
462 else:
452 else:
463 break
453 break
464 else:
454
465 prefix = long_substr(list(matches))
455 text = "\n".join([ln[strip_len:] for ln in lines])
466 if prefix:
467 strip = len(prefix)
468 text = '\n'.join([ ln[strip:] for ln in lines])
469 return text
456 return text
470
457
458
471 def strip_ansi(source):
459 def strip_ansi(source):
472 """
460 """
473 Remove ansi escape codes from text.
461 Remove ansi escape codes from text.
474
462
475 Parameters
463 Parameters
476 ----------
464 ----------
477 source : str
465 source : str
478 Source to remove the ansi from
466 Source to remove the ansi from
479 """
467 """
480 return re.sub(r'\033\[(\d|;)+?m', '', source)
468 return re.sub(r'\033\[(\d|;)+?m', '', source)
481
469
482
470
483 class EvalFormatter(Formatter):
471 class EvalFormatter(Formatter):
484 """A String Formatter that allows evaluation of simple expressions.
472 """A String Formatter that allows evaluation of simple expressions.
485
473
486 Note that this version interprets a : as specifying a format string (as per
474 Note that this version interprets a : as specifying a format string (as per
487 standard string formatting), so if slicing is required, you must explicitly
475 standard string formatting), so if slicing is required, you must explicitly
488 create a slice.
476 create a slice.
489
477
490 This is to be used in templating cases, such as the parallel batch
478 This is to be used in templating cases, such as the parallel batch
491 script templates, where simple arithmetic on arguments is useful.
479 script templates, where simple arithmetic on arguments is useful.
492
480
493 Examples
481 Examples
494 --------
482 --------
495 ::
483 ::
496
484
497 In [1]: f = EvalFormatter()
485 In [1]: f = EvalFormatter()
498 In [2]: f.format('{n//4}', n=8)
486 In [2]: f.format('{n//4}', n=8)
499 Out[2]: '2'
487 Out[2]: '2'
500
488
501 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
489 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
502 Out[3]: 'll'
490 Out[3]: 'll'
503 """
491 """
504 def get_field(self, name, args, kwargs):
492 def get_field(self, name, args, kwargs):
505 v = eval(name, kwargs)
493 v = eval(name, kwargs)
506 return v, name
494 return v, name
507
495
508 #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
496 #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
509 # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
497 # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
510 # above, it should be possible to remove FullEvalFormatter.
498 # above, it should be possible to remove FullEvalFormatter.
511
499
512 class FullEvalFormatter(Formatter):
500 class FullEvalFormatter(Formatter):
513 """A String Formatter that allows evaluation of simple expressions.
501 """A String Formatter that allows evaluation of simple expressions.
514
502
515 Any time a format key is not found in the kwargs,
503 Any time a format key is not found in the kwargs,
516 it will be tried as an expression in the kwargs namespace.
504 it will be tried as an expression in the kwargs namespace.
517
505
518 Note that this version allows slicing using [1:2], so you cannot specify
506 Note that this version allows slicing using [1:2], so you cannot specify
519 a format string. Use :class:`EvalFormatter` to permit format strings.
507 a format string. Use :class:`EvalFormatter` to permit format strings.
520
508
521 Examples
509 Examples
522 --------
510 --------
523 ::
511 ::
524
512
525 In [1]: f = FullEvalFormatter()
513 In [1]: f = FullEvalFormatter()
526 In [2]: f.format('{n//4}', n=8)
514 In [2]: f.format('{n//4}', n=8)
527 Out[2]: '2'
515 Out[2]: '2'
528
516
529 In [3]: f.format('{list(range(5))[2:4]}')
517 In [3]: f.format('{list(range(5))[2:4]}')
530 Out[3]: '[2, 3]'
518 Out[3]: '[2, 3]'
531
519
532 In [4]: f.format('{3*2}')
520 In [4]: f.format('{3*2}')
533 Out[4]: '6'
521 Out[4]: '6'
534 """
522 """
535 # copied from Formatter._vformat with minor changes to allow eval
523 # copied from Formatter._vformat with minor changes to allow eval
536 # and replace the format_spec code with slicing
524 # and replace the format_spec code with slicing
537 def vformat(self, format_string:str, args, kwargs)->str:
525 def vformat(self, format_string:str, args, kwargs)->str:
538 result = []
526 result = []
539 for literal_text, field_name, format_spec, conversion in \
527 for literal_text, field_name, format_spec, conversion in \
540 self.parse(format_string):
528 self.parse(format_string):
541
529
542 # output the literal text
530 # output the literal text
543 if literal_text:
531 if literal_text:
544 result.append(literal_text)
532 result.append(literal_text)
545
533
546 # if there's a field, output it
534 # if there's a field, output it
547 if field_name is not None:
535 if field_name is not None:
548 # this is some markup, find the object and do
536 # this is some markup, find the object and do
549 # the formatting
537 # the formatting
550
538
551 if format_spec:
539 if format_spec:
552 # override format spec, to allow slicing:
540 # override format spec, to allow slicing:
553 field_name = ':'.join([field_name, format_spec])
541 field_name = ':'.join([field_name, format_spec])
554
542
555 # eval the contents of the field for the object
543 # eval the contents of the field for the object
556 # to be formatted
544 # to be formatted
557 obj = eval(field_name, kwargs)
545 obj = eval(field_name, kwargs)
558
546
559 # do any conversion on the resulting object
547 # do any conversion on the resulting object
560 obj = self.convert_field(obj, conversion)
548 obj = self.convert_field(obj, conversion)
561
549
562 # format the object and append to the result
550 # format the object and append to the result
563 result.append(self.format_field(obj, ''))
551 result.append(self.format_field(obj, ''))
564
552
565 return ''.join(result)
553 return ''.join(result)
566
554
567
555
568 class DollarFormatter(FullEvalFormatter):
556 class DollarFormatter(FullEvalFormatter):
569 """Formatter allowing Itpl style $foo replacement, for names and attribute
557 """Formatter allowing Itpl style $foo replacement, for names and attribute
570 access only. Standard {foo} replacement also works, and allows full
558 access only. Standard {foo} replacement also works, and allows full
571 evaluation of its arguments.
559 evaluation of its arguments.
572
560
573 Examples
561 Examples
574 --------
562 --------
575 ::
563 ::
576
564
577 In [1]: f = DollarFormatter()
565 In [1]: f = DollarFormatter()
578 In [2]: f.format('{n//4}', n=8)
566 In [2]: f.format('{n//4}', n=8)
579 Out[2]: '2'
567 Out[2]: '2'
580
568
581 In [3]: f.format('23 * 76 is $result', result=23*76)
569 In [3]: f.format('23 * 76 is $result', result=23*76)
582 Out[3]: '23 * 76 is 1748'
570 Out[3]: '23 * 76 is 1748'
583
571
584 In [4]: f.format('$a or {b}', a=1, b=2)
572 In [4]: f.format('$a or {b}', a=1, b=2)
585 Out[4]: '1 or 2'
573 Out[4]: '1 or 2'
586 """
574 """
587 _dollar_pattern_ignore_single_quote = re.compile(r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)")
575 _dollar_pattern_ignore_single_quote = re.compile(r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)")
588 def parse(self, fmt_string):
576 def parse(self, fmt_string):
589 for literal_txt, field_name, format_spec, conversion \
577 for literal_txt, field_name, format_spec, conversion \
590 in Formatter.parse(self, fmt_string):
578 in Formatter.parse(self, fmt_string):
591
579
592 # Find $foo patterns in the literal text.
580 # Find $foo patterns in the literal text.
593 continue_from = 0
581 continue_from = 0
594 txt = ""
582 txt = ""
595 for m in self._dollar_pattern_ignore_single_quote.finditer(literal_txt):
583 for m in self._dollar_pattern_ignore_single_quote.finditer(literal_txt):
596 new_txt, new_field = m.group(1,2)
584 new_txt, new_field = m.group(1,2)
597 # $$foo --> $foo
585 # $$foo --> $foo
598 if new_field.startswith("$"):
586 if new_field.startswith("$"):
599 txt += new_txt + new_field
587 txt += new_txt + new_field
600 else:
588 else:
601 yield (txt + new_txt, new_field, "", None)
589 yield (txt + new_txt, new_field, "", None)
602 txt = ""
590 txt = ""
603 continue_from = m.end()
591 continue_from = m.end()
604
592
605 # Re-yield the {foo} style pattern
593 # Re-yield the {foo} style pattern
606 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
594 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
607
595
608 #-----------------------------------------------------------------------------
596 #-----------------------------------------------------------------------------
609 # Utils to columnize a list of string
597 # Utils to columnize a list of string
610 #-----------------------------------------------------------------------------
598 #-----------------------------------------------------------------------------
611
599
612 def _col_chunks(l, max_rows, row_first=False):
600 def _col_chunks(l, max_rows, row_first=False):
613 """Yield successive max_rows-sized column chunks from l."""
601 """Yield successive max_rows-sized column chunks from l."""
614 if row_first:
602 if row_first:
615 ncols = (len(l) // max_rows) + (len(l) % max_rows > 0)
603 ncols = (len(l) // max_rows) + (len(l) % max_rows > 0)
616 for i in range(ncols):
604 for i in range(ncols):
617 yield [l[j] for j in range(i, len(l), ncols)]
605 yield [l[j] for j in range(i, len(l), ncols)]
618 else:
606 else:
619 for i in range(0, len(l), max_rows):
607 for i in range(0, len(l), max_rows):
620 yield l[i:(i + max_rows)]
608 yield l[i:(i + max_rows)]
621
609
622
610
623 def _find_optimal(rlist, row_first=False, separator_size=2, displaywidth=80):
611 def _find_optimal(rlist, row_first=False, separator_size=2, displaywidth=80):
624 """Calculate optimal info to columnize a list of string"""
612 """Calculate optimal info to columnize a list of string"""
625 for max_rows in range(1, len(rlist) + 1):
613 for max_rows in range(1, len(rlist) + 1):
626 col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first)))
614 col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first)))
627 sumlength = sum(col_widths)
615 sumlength = sum(col_widths)
628 ncols = len(col_widths)
616 ncols = len(col_widths)
629 if sumlength + separator_size * (ncols - 1) <= displaywidth:
617 if sumlength + separator_size * (ncols - 1) <= displaywidth:
630 break
618 break
631 return {'num_columns': ncols,
619 return {'num_columns': ncols,
632 'optimal_separator_width': (displaywidth - sumlength) // (ncols - 1) if (ncols - 1) else 0,
620 'optimal_separator_width': (displaywidth - sumlength) // (ncols - 1) if (ncols - 1) else 0,
633 'max_rows': max_rows,
621 'max_rows': max_rows,
634 'column_widths': col_widths
622 'column_widths': col_widths
635 }
623 }
636
624
637
625
638 def _get_or_default(mylist, i, default=None):
626 def _get_or_default(mylist, i, default=None):
639 """return list item number, or default if don't exist"""
627 """return list item number, or default if don't exist"""
640 if i >= len(mylist):
628 if i >= len(mylist):
641 return default
629 return default
642 else :
630 else :
643 return mylist[i]
631 return mylist[i]
644
632
645
633
646 def compute_item_matrix(items, row_first=False, empty=None, *args, **kwargs) :
634 def compute_item_matrix(items, row_first=False, empty=None, *args, **kwargs) :
647 """Returns a nested list, and info to columnize items
635 """Returns a nested list, and info to columnize items
648
636
649 Parameters
637 Parameters
650 ----------
638 ----------
651 items
639 items
652 list of strings to columize
640 list of strings to columize
653 row_first : (default False)
641 row_first : (default False)
654 Whether to compute columns for a row-first matrix instead of
642 Whether to compute columns for a row-first matrix instead of
655 column-first (default).
643 column-first (default).
656 empty : (default None)
644 empty : (default None)
657 default value to fill list if needed
645 default value to fill list if needed
658 separator_size : int (default=2)
646 separator_size : int (default=2)
659 How much characters will be used as a separation between each columns.
647 How much characters will be used as a separation between each columns.
660 displaywidth : int (default=80)
648 displaywidth : int (default=80)
661 The width of the area onto which the columns should enter
649 The width of the area onto which the columns should enter
662
650
663 Returns
651 Returns
664 -------
652 -------
665 strings_matrix
653 strings_matrix
666 nested list of string, the outer most list contains as many list as
654 nested list of string, the outer most list contains as many list as
667 rows, the innermost lists have each as many element as columns. If the
655 rows, the innermost lists have each as many element as columns. If the
668 total number of elements in `items` does not equal the product of
656 total number of elements in `items` does not equal the product of
669 rows*columns, the last element of some lists are filled with `None`.
657 rows*columns, the last element of some lists are filled with `None`.
670 dict_info
658 dict_info
671 some info to make columnize easier:
659 some info to make columnize easier:
672
660
673 num_columns
661 num_columns
674 number of columns
662 number of columns
675 max_rows
663 max_rows
676 maximum number of rows (final number may be less)
664 maximum number of rows (final number may be less)
677 column_widths
665 column_widths
678 list of with of each columns
666 list of with of each columns
679 optimal_separator_width
667 optimal_separator_width
680 best separator width between columns
668 best separator width between columns
681
669
682 Examples
670 Examples
683 --------
671 --------
684 ::
672 ::
685
673
686 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
674 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
687 In [2]: list, info = compute_item_matrix(l, displaywidth=12)
675 In [2]: list, info = compute_item_matrix(l, displaywidth=12)
688 In [3]: list
676 In [3]: list
689 Out[3]: [['aaa', 'f', 'k'], ['b', 'g', 'l'], ['cc', 'h', None], ['d', 'i', None], ['eeeee', 'j', None]]
677 Out[3]: [['aaa', 'f', 'k'], ['b', 'g', 'l'], ['cc', 'h', None], ['d', 'i', None], ['eeeee', 'j', None]]
690 In [4]: ideal = {'num_columns': 3, 'column_widths': [5, 1, 1], 'optimal_separator_width': 2, 'max_rows': 5}
678 In [4]: ideal = {'num_columns': 3, 'column_widths': [5, 1, 1], 'optimal_separator_width': 2, 'max_rows': 5}
691 In [5]: all((info[k] == ideal[k] for k in ideal.keys()))
679 In [5]: all((info[k] == ideal[k] for k in ideal.keys()))
692 Out[5]: True
680 Out[5]: True
693 """
681 """
694 info = _find_optimal(list(map(len, items)), row_first, *args, **kwargs)
682 info = _find_optimal(list(map(len, items)), row_first, *args, **kwargs)
695 nrow, ncol = info['max_rows'], info['num_columns']
683 nrow, ncol = info['max_rows'], info['num_columns']
696 if row_first:
684 if row_first:
697 return ([[_get_or_default(items, r * ncol + c, default=empty) for c in range(ncol)] for r in range(nrow)], info)
685 return ([[_get_or_default(items, r * ncol + c, default=empty) for c in range(ncol)] for r in range(nrow)], info)
698 else:
686 else:
699 return ([[_get_or_default(items, c * nrow + r, default=empty) for c in range(ncol)] for r in range(nrow)], info)
687 return ([[_get_or_default(items, c * nrow + r, default=empty) for c in range(ncol)] for r in range(nrow)], info)
700
688
701
689
702 def columnize(items, row_first=False, separator=' ', displaywidth=80, spread=False):
690 def columnize(items, row_first=False, separator=' ', displaywidth=80, spread=False):
703 """ Transform a list of strings into a single string with columns.
691 """ Transform a list of strings into a single string with columns.
704
692
705 Parameters
693 Parameters
706 ----------
694 ----------
707 items : sequence of strings
695 items : sequence of strings
708 The strings to process.
696 The strings to process.
709 row_first : (default False)
697 row_first : (default False)
710 Whether to compute columns for a row-first matrix instead of
698 Whether to compute columns for a row-first matrix instead of
711 column-first (default).
699 column-first (default).
712 separator : str, optional [default is two spaces]
700 separator : str, optional [default is two spaces]
713 The string that separates columns.
701 The string that separates columns.
714 displaywidth : int, optional [default is 80]
702 displaywidth : int, optional [default is 80]
715 Width of the display in number of characters.
703 Width of the display in number of characters.
716
704
717 Returns
705 Returns
718 -------
706 -------
719 The formatted string.
707 The formatted string.
720 """
708 """
721 if not items:
709 if not items:
722 return '\n'
710 return '\n'
723 matrix, info = compute_item_matrix(items, row_first=row_first, separator_size=len(separator), displaywidth=displaywidth)
711 matrix, info = compute_item_matrix(items, row_first=row_first, separator_size=len(separator), displaywidth=displaywidth)
724 if spread:
712 if spread:
725 separator = separator.ljust(int(info['optimal_separator_width']))
713 separator = separator.ljust(int(info['optimal_separator_width']))
726 fmatrix = [filter(None, x) for x in matrix]
714 fmatrix = [filter(None, x) for x in matrix]
727 sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['column_widths'])])
715 sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['column_widths'])])
728 return '\n'.join(map(sjoin, fmatrix))+'\n'
716 return '\n'.join(map(sjoin, fmatrix))+'\n'
729
717
730
718
731 def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""):
719 def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""):
732 """
720 """
733 Return a string with a natural enumeration of items
721 Return a string with a natural enumeration of items
734
722
735 >>> get_text_list(['a', 'b', 'c', 'd'])
723 >>> get_text_list(['a', 'b', 'c', 'd'])
736 'a, b, c and d'
724 'a, b, c and d'
737 >>> get_text_list(['a', 'b', 'c'], ' or ')
725 >>> get_text_list(['a', 'b', 'c'], ' or ')
738 'a, b or c'
726 'a, b or c'
739 >>> get_text_list(['a', 'b', 'c'], ', ')
727 >>> get_text_list(['a', 'b', 'c'], ', ')
740 'a, b, c'
728 'a, b, c'
741 >>> get_text_list(['a', 'b'], ' or ')
729 >>> get_text_list(['a', 'b'], ' or ')
742 'a or b'
730 'a or b'
743 >>> get_text_list(['a'])
731 >>> get_text_list(['a'])
744 'a'
732 'a'
745 >>> get_text_list([])
733 >>> get_text_list([])
746 ''
734 ''
747 >>> get_text_list(['a', 'b'], wrap_item_with="`")
735 >>> get_text_list(['a', 'b'], wrap_item_with="`")
748 '`a` and `b`'
736 '`a` and `b`'
749 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
737 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
750 'a + b + c = d'
738 'a + b + c = d'
751 """
739 """
752 if len(list_) == 0:
740 if len(list_) == 0:
753 return ''
741 return ''
754 if wrap_item_with:
742 if wrap_item_with:
755 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
743 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
756 item in list_]
744 item in list_]
757 if len(list_) == 1:
745 if len(list_) == 1:
758 return list_[0]
746 return list_[0]
759 return '%s%s%s' % (
747 return '%s%s%s' % (
760 sep.join(i for i in list_[:-1]),
748 sep.join(i for i in list_[:-1]),
761 last_sep, list_[-1])
749 last_sep, list_[-1])
General Comments 0
You need to be logged in to leave comments. Login now