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