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