blob: 852215de26196d018a39f17f0cfc18c5bd76c6e9 [file] [log] [blame]
Rich Laneb658ddd2013-03-12 10:15:10 -07001"""
2pp - port of Ruby's PP library
3Also based on Lindig, C., & GbR, G. D. (2000). Strictly Pretty.
4
5Example usage:
6>>> import pp.pp as pp
7>>> print pp([[1, 2], [3, 4]], maxwidth=15)
8[
9 [ 1, 2 ],
10 [ 3, 4 ]
11]
12"""
13import unittest
14from contextlib import contextmanager
15
16def pp(obj, maxwidth=79):
17 """
18 Pretty-print the given object.
19 """
20 ctx = PrettyPrinter(maxwidth=maxwidth)
21 ctx.pp(obj)
22 return str(ctx)
23
24
25## Pretty-printers for builtin classes
26
27def pretty_print_list(pp, obj):
28 with pp.group():
29 pp.text('[')
30 with pp.indent(2):
31 for v in obj:
32 if not pp.first(): pp.text(',')
33 pp.breakable()
34 pp.pp(v)
35 pp.breakable()
36 pp.text(']')
37
38def pretty_print_dict(pp, obj):
39 with pp.group():
40 pp.text('{')
41 with pp.indent(2):
42 for (k, v) in sorted(obj.items()):
43 if not pp.first(): pp.text(',')
44 pp.breakable()
45 pp.pp(k)
46 pp.text(': ')
47 pp.pp(v)
48 pp.breakable()
49 pp.text('}')
50
51pretty_printers = {
52 list: pretty_print_list,
53 dict: pretty_print_dict,
54}
55
56
57## Implementation
58
59class PrettyPrinter(object):
60 def __init__(self, maxwidth):
61 self.maxwidth = maxwidth
62 self.cur_indent = 0
63 self.root_group = Group()
64 self.group_stack = [self.root_group]
65
66 def current_group(self):
67 return self.group_stack[-1]
68
69 def text(self, s):
70 self.current_group().append(str(s))
71
72 def breakable(self, sep=' '):
73 self.current_group().append(Breakable(sep, self.cur_indent))
74
75 def first(self):
76 return self.current_group().first()
77
78 @contextmanager
79 def indent(self, n):
80 self.cur_indent += n
81 yield
82 self.cur_indent -= n
83
84 @contextmanager
85 def group(self):
86 self.group_stack.append(Group())
87 yield
88 new_group = self.group_stack.pop()
89 self.current_group().append(new_group)
90
91 def pp(self, obj):
92 if hasattr(obj, "pretty_print"):
93 obj.pretty_print(self)
94 elif type(obj) in pretty_printers:
95 pretty_printers[type(obj)](self, obj)
96 else:
97 self.text(repr(obj))
98
99 def __str__(self):
100 return self.root_group.render(0, self.maxwidth)
101
102class Group(object):
103 __slots__ = ["fragments", "length", "_first"]
104
105 def __init__(self):
106 self.fragments = []
107 self.length = 0
108 self._first = True
109
110 def append(self, x):
111 self.fragments.append(x)
112 self.length += len(x)
113
114 def first(self):
115 if self._first:
116 self._first = False
117 return True
118 return False
119
120 def __len__(self):
121 return self.length
122
123 def render(self, curwidth, maxwidth):
124 dobreak = len(self) > (maxwidth - curwidth)
125
126 a = []
127 for x in self.fragments:
128 if isinstance(x, Breakable):
129 if dobreak:
130 a.append('\n')
131 a.append(' ' * x.indent)
132 curwidth = 0
133 else:
134 a.append(x.sep)
135 elif isinstance(x, Group):
136 a.append(x.render(curwidth, maxwidth))
137 else:
138 a.append(x)
139 curwidth += len(a[-1])
140 return ''.join(a)
141
142class Breakable(object):
143 __slots__ = ["sep", "indent"]
144
145 def __init__(self, sep, indent):
146 self.sep = sep
147 self.indent = indent
148
149 def __len__(self):
150 return len(self.sep)
151
152
153## Tests
154
155class TestPP(unittest.TestCase):
156 def test_scalars(self):
157 self.assertEquals(pp(1), "1")
158 self.assertEquals(pp("foo"), "'foo'")
159
160 def test_hash(self):
161 expected = """{ 1: 'a', 'b': 2 }"""
162 self.assertEquals(pp(eval(expected)), expected)
163 expected = """\
164{
165 1: 'a',
166 'b': 2
167}"""
168 self.assertEquals(pp(eval(expected), maxwidth=0), expected)
169
170 def test_array(self):
171 expected = """[ 1, 'a', 2 ]"""
172 self.assertEquals(pp(eval(expected)), expected)
173 expected = """\
174[
175 1,
176 'a',
177 2
178]"""
179 self.assertEquals(pp(eval(expected), maxwidth=0), expected)
180
181 def test_nested(self):
182 expected = """[ [ 1, 2 ], [ 3, 4 ] ]"""
183 self.assertEquals(pp(eval(expected)), expected)
184 expected = """\
185[
186 [
187 1,
188 2
189 ],
190 [
191 3,
192 4
193 ]
194]"""
195 self.assertEquals(pp(eval(expected), maxwidth=0), expected)
196
197 def test_breaking(self):
198 expected = """\
199[
200 [ 1, 2 ],
201 'abcdefghijklmnopqrstuvwxyz'
202]"""
203 self.assertEquals(pp(eval(expected), maxwidth=24), expected)
204 expected = """\
205[
206 [ 'abcd', 2 ],
207 [ '0123456789' ],
208 [
209 '0123456789',
210 'abcdefghij'
211 ],
212 [ 'abcdefghijklmnop' ],
213 [
214 'abcdefghijklmnopq'
215 ],
216 { 'k': 'v' },
217 {
218 1: [ 2, [ 3, 4 ] ],
219 'foo': 'abcdefghijklmnop'
220 }
221]"""
222 self.assertEquals(pp(eval(expected), maxwidth=24), expected)
223 expected = """\
224[
225 [ 1, 2 ],
226 [ 3, 4 ]
227]"""
228 self.assertEquals(pp(eval(expected), maxwidth=15), expected)
229
230 # This is an edge case where our simpler algorithm breaks down.
231 @unittest.expectedFailure
232 def test_greedy_breaking(self):
233 expected = """\
234abc def
235ghijklmnopqrstuvwxyz\
236"""
237 pp = PrettyPrinter(maxwidth=8)
238 pp.text("abc")
239 with pp.group():
240 pp.breakable()
241 pp.text("def")
242 with pp.group():
243 pp.breakable()
244 pp.text("ghijklmnopqrstuvwxyz")
245 self.assertEquals(str(pp), expected)
246
247if __name__ == '__main__':
248 unittest.main()