OILS / asdl / format.py View on Github | oilshell.org

535 lines, 303 significant
1"""
2format.py -- Pretty print an ASDL data structure.
3
4TODO: replace ad hoc line wrapper, e.g. _TrySingleLine
5
6- auto-abbreviation of single field things (minus location)
7- option to omit spaces for SQ, SQ, W? It's all one thing.
8
9Where we try wrap to a single line:
10 - arrays
11 - objects with name fields
12 - abbreviated, unnamed fields
13"""
14from typing import Tuple, List
15
16from _devbuild.gen.hnode_asdl import (hnode, hnode_e, hnode_t, hnode_str,
17 color_e, color_t)
18from data_lang import j8_lite
19from display import ansi
20from display import pretty
21from pylib import cgi
22from mycpp import mylib
23
24from typing import cast, Any, Optional
25
26if mylib.PYTHON:
27
28 def PrettyPrint(obj, f=None):
29 # type: (Any, Optional[mylib.Writer]) -> None
30 """Print abbreviated tree in color. For unit tests."""
31 f = f if f else mylib.Stdout()
32
33 ast_f = DetectConsoleOutput(f)
34 tree = obj.AbbreviatedTree()
35 PrintTree(tree, ast_f)
36
37
38def DetectConsoleOutput(f):
39 # type: (mylib.Writer) -> ColorOutput
40 """Wrapped to auto-detect."""
41 if f.isatty():
42 return AnsiOutput(f)
43 else:
44 return TextOutput(f)
45
46
47class ColorOutput(object):
48 """Abstract base class for plain text, ANSI color, and HTML color."""
49
50 def __init__(self, f):
51 # type: (mylib.Writer) -> None
52 self.f = f
53 self.num_chars = 0
54
55 def NewTempBuffer(self):
56 # type: () -> ColorOutput
57 """Return a temporary buffer for the line wrapping calculation."""
58 raise NotImplementedError()
59
60 def FileHeader(self):
61 # type: () -> None
62 """Hook for printing a full file."""
63 pass
64
65 def FileFooter(self):
66 # type: () -> None
67 """Hook for printing a full file."""
68 pass
69
70 def PushColor(self, e_color):
71 # type: (color_t) -> None
72 raise NotImplementedError()
73
74 def PopColor(self):
75 # type: () -> None
76 raise NotImplementedError()
77
78 def write(self, s):
79 # type: (str) -> None
80 self.f.write(s)
81 self.num_chars += len(s) # Only count visible characters!
82
83 def WriteRaw(self, raw):
84 # type: (Tuple[str, int]) -> None
85 """Write raw data without escaping, and without counting control codes
86 in the length."""
87 s, num_chars = raw
88 self.f.write(s)
89 self.num_chars += num_chars
90
91 def NumChars(self):
92 # type: () -> int
93 return self.num_chars
94
95 def GetRaw(self):
96 # type: () -> Tuple[str, int]
97
98 # NOTE: Ensured by NewTempBuffer()
99 f = cast(mylib.BufWriter, self.f)
100 return f.getvalue(), self.num_chars
101
102
103class TextOutput(ColorOutput):
104 """TextOutput put obeys the color interface, but outputs nothing."""
105
106 def __init__(self, f):
107 # type: (mylib.Writer) -> None
108 ColorOutput.__init__(self, f)
109
110 def NewTempBuffer(self):
111 # type: () -> TextOutput
112 return TextOutput(mylib.BufWriter())
113
114 def PushColor(self, e_color):
115 # type: (color_t) -> None
116 pass # ignore color
117
118 def PopColor(self):
119 # type: () -> None
120 pass # ignore color
121
122
123class HtmlOutput(ColorOutput):
124 """HTML one can have wider columns. Maybe not even fixed-width font. Hm
125 yeah indentation should be logical then?
126
127 Color: HTML spans
128 """
129
130 def __init__(self, f):
131 # type: (mylib.Writer) -> None
132 ColorOutput.__init__(self, f)
133
134 def NewTempBuffer(self):
135 # type: () -> HtmlOutput
136 return HtmlOutput(mylib.BufWriter())
137
138 def FileHeader(self):
139 # type: () -> None
140 # TODO: Use a different CSS file to make the colors match. I like string
141 # literals as yellow, etc.
142 #<link rel="stylesheet" type="text/css" href="/css/code.css" />
143 self.f.write("""
144<html>
145 <head>
146 <title>Oils AST</title>
147 <style>
148 .n { color: brown }
149 .s { font-weight: bold }
150 .o { color: darkgreen }
151 </style>
152 </head>
153 <body>
154 <pre>
155""")
156
157 def FileFooter(self):
158 # type: () -> None
159 self.f.write("""
160 </pre>
161 </body>
162</html>
163 """)
164
165 def PushColor(self, e_color):
166 # type: (color_t) -> None
167 # To save bandwidth, use single character CSS names.
168 if e_color == color_e.TypeName:
169 css_class = 'n'
170 elif e_color == color_e.StringConst:
171 css_class = 's'
172 elif e_color == color_e.OtherConst:
173 css_class = 'o'
174 elif e_color == color_e.External:
175 css_class = 'o'
176 elif e_color == color_e.UserType:
177 css_class = 'o'
178 else:
179 raise AssertionError(e_color)
180 self.f.write('<span class="%s">' % css_class)
181
182 def PopColor(self):
183 # type: () -> None
184 self.f.write('</span>')
185
186 def write(self, s):
187 # type: (str) -> None
188
189 # PROBLEM: Double escaping!
190 self.f.write(cgi.escape(s))
191 self.num_chars += len(s) # Only count visible characters!
192
193
194class AnsiOutput(ColorOutput):
195 """For the console."""
196
197 def __init__(self, f):
198 # type: (mylib.Writer) -> None
199 ColorOutput.__init__(self, f)
200
201 def NewTempBuffer(self):
202 # type: () -> AnsiOutput
203 return AnsiOutput(mylib.BufWriter())
204
205 def PushColor(self, e_color):
206 # type: (color_t) -> None
207 if e_color == color_e.TypeName:
208 self.f.write(ansi.YELLOW)
209 elif e_color == color_e.StringConst:
210 self.f.write(ansi.BOLD)
211 elif e_color == color_e.OtherConst:
212 self.f.write(ansi.GREEN)
213 elif e_color == color_e.External:
214 self.f.write(ansi.BOLD + ansi.BLUE)
215 elif e_color == color_e.UserType:
216 self.f.write(ansi.GREEN) # Same color as other literals for now
217 else:
218 raise AssertionError(e_color)
219
220 def PopColor(self):
221 # type: () -> None
222 self.f.write(ansi.RESET)
223
224
225INDENT = 2
226
227
228class _PrettyPrinter(object):
229
230 def __init__(self, max_col):
231 # type: (int) -> None
232 self.max_col = max_col
233
234 def _PrintWrappedArray(self, array, prefix_len, f, indent):
235 # type: (List[hnode_t], int, ColorOutput, int) -> bool
236 """Print an array of objects with line wrapping.
237
238 Returns whether they all fit on a single line, so you can print
239 the closing brace properly.
240 """
241 all_fit = True
242 chars_so_far = prefix_len
243
244 for i, val in enumerate(array):
245 if i != 0:
246 f.write(' ')
247
248 single_f = f.NewTempBuffer()
249 if _TrySingleLine(val, single_f, self.max_col - chars_so_far):
250 s, num_chars = single_f.GetRaw() # extra unpacking for mycpp
251 f.WriteRaw((s, num_chars))
252 chars_so_far += single_f.NumChars()
253 else: # WRAP THE LINE
254 f.write('\n')
255 self.PrintNode(val, f, indent + INDENT)
256
257 chars_so_far = 0 # allow more
258 all_fit = False
259 return all_fit
260
261 def _PrintWholeArray(self, array, prefix_len, f, indent):
262 # type: (List[hnode_t], int, ColorOutput, int) -> bool
263
264 # This is UNLIKE the abbreviated case above, where we do WRAPPING.
265 # Here, ALL children must fit on a single line, or else we separate
266 # each one onto a separate line. This is to avoid the following:
267 #
268 # children: [(C ...)
269 # (C ...)
270 # ]
271 # The first child is out of line. The abbreviated objects have a
272 # small header like C or DQ so it doesn't matter as much.
273 all_fit = True
274 pieces = [] # type: List[Tuple[str, int]]
275 chars_so_far = prefix_len
276 for item in array:
277 single_f = f.NewTempBuffer()
278 if _TrySingleLine(item, single_f, self.max_col - chars_so_far):
279 s, num_chars = single_f.GetRaw() # extra unpacking for mycpp
280 pieces.append((s, num_chars))
281 chars_so_far += single_f.NumChars()
282 else:
283 all_fit = False
284 break
285
286 if all_fit:
287 for i, p in enumerate(pieces):
288 if i != 0:
289 f.write(' ')
290 f.WriteRaw(p)
291 f.write(']')
292 return all_fit
293
294 def _PrintRecord(self, node, f, indent):
295 # type: (hnode.Record, ColorOutput, int) -> None
296 """Print a CompoundObj in abbreviated or normal form."""
297 ind = ' ' * indent
298
299 if node.abbrev: # abbreviated
300 prefix = ind + node.left
301 f.write(prefix)
302 if len(node.node_type):
303 f.PushColor(color_e.TypeName)
304 f.write(node.node_type)
305 f.PopColor()
306 f.write(' ')
307
308 prefix_len = len(prefix) + len(node.node_type) + 1
309 all_fit = self._PrintWrappedArray(node.unnamed_fields, prefix_len,
310 f, indent)
311
312 if not all_fit:
313 f.write('\n')
314 f.write(ind)
315 f.write(node.right)
316
317 else: # full form like (SimpleCommand ...)
318 f.write(ind + node.left)
319
320 f.PushColor(color_e.TypeName)
321 f.write(node.node_type)
322 f.PopColor()
323
324 f.write('\n')
325 for field in node.fields:
326 name = field.name
327 val = field.val
328
329 ind1 = ' ' * (indent + INDENT)
330 UP_val = val # for mycpp
331 tag = val.tag()
332 if tag == hnode_e.Array:
333 val = cast(hnode.Array, UP_val)
334
335 name_str = '%s%s: [' % (ind1, name)
336 f.write(name_str)
337 prefix_len = len(name_str)
338
339 if not self._PrintWholeArray(val.children, prefix_len, f,
340 indent):
341 f.write('\n')
342 for child in val.children:
343 self.PrintNode(child, f, indent + INDENT + INDENT)
344 f.write('\n')
345 f.write('%s]' % ind1)
346
347 else: # primitive field
348 name_str = '%s%s: ' % (ind1, name)
349 f.write(name_str)
350 prefix_len = len(name_str)
351
352 # Try to print it on the same line as the field name; otherwise print
353 # it on a separate line.
354 single_f = f.NewTempBuffer()
355 if _TrySingleLine(val, single_f,
356 self.max_col - prefix_len):
357 s, num_chars = single_f.GetRaw(
358 ) # extra unpacking for mycpp
359 f.WriteRaw((s, num_chars))
360 else:
361 f.write('\n')
362 self.PrintNode(val, f, indent + INDENT + INDENT)
363
364 f.write('\n') # separate fields
365
366 f.write(ind + node.right)
367
368 def PrintNode(self, node, f, indent):
369 # type: (hnode_t, ColorOutput, int) -> None
370 """Second step of printing: turn homogeneous tree into a colored
371 string.
372
373 Args:
374 node: homogeneous tree node
375 f: ColorOutput instance.
376 max_col: don't print past this column number on ANY line
377 NOTE: See asdl/run.sh line-length-hist for a test of this. It's
378 approximate.
379 TODO: Use the terminal width.
380 """
381 ind = ' ' * indent
382
383 # Try printing on a single line
384 single_f = f.NewTempBuffer()
385 single_f.write(ind)
386 if _TrySingleLine(node, single_f, self.max_col - indent):
387 s, num_chars = single_f.GetRaw() # extra unpacking for mycpp
388 f.WriteRaw((s, num_chars))
389 return
390
391 UP_node = node # for mycpp
392 tag = node.tag()
393 if tag == hnode_e.Leaf:
394 node = cast(hnode.Leaf, UP_node)
395 f.PushColor(node.color)
396 f.write(j8_lite.EncodeString(node.s, unquoted_ok=True))
397 f.PopColor()
398
399 elif tag == hnode_e.External:
400 node = cast(hnode.External, UP_node)
401 f.PushColor(color_e.External)
402 if mylib.PYTHON:
403 f.write(repr(node.obj))
404 else:
405 f.write('UNTYPED any')
406 f.PopColor()
407
408 elif tag == hnode_e.Record:
409 node = cast(hnode.Record, UP_node)
410 self._PrintRecord(node, f, indent)
411
412 elif tag == hnode_e.AlreadySeen:
413 node = cast(hnode.AlreadySeen, UP_node)
414 # ... means omitting second reference, while --- means a cycle
415 f.write('...0x%s' % mylib.hex_lower(node.heap_id))
416
417 else:
418 raise AssertionError(node)
419
420
421def _TrySingleLineObj(node, f, max_chars):
422 # type: (hnode.Record, ColorOutput, int) -> bool
423 """Print an object on a single line."""
424 f.write(node.left)
425 if node.abbrev:
426 if len(node.node_type):
427 f.PushColor(color_e.TypeName)
428 f.write(node.node_type)
429 f.PopColor()
430 f.write(' ')
431
432 for i, val in enumerate(node.unnamed_fields):
433 if i != 0:
434 f.write(' ')
435 if not _TrySingleLine(val, f, max_chars):
436 return False
437 else:
438 f.PushColor(color_e.TypeName)
439 f.write(node.node_type)
440 f.PopColor()
441
442 for field in node.fields:
443 f.write(' %s:' % field.name)
444 if not _TrySingleLine(field.val, f, max_chars):
445 return False
446
447 f.write(node.right)
448 return True
449
450
451def _TrySingleLine(node, f, max_chars):
452 # type: (hnode_t, ColorOutput, int) -> bool
453 """Try printing on a single line.
454
455 Args:
456 node: homogeneous tree node
457 f: ColorOutput instance
458 max_chars: maximum number of characters to print on THIS line
459 indent: current indent level
460
461 Returns:
462 ok: whether it fit on the line of the given size.
463 If False, you can't use the value of f.
464 """
465 UP_node = node # for mycpp
466 tag = node.tag()
467 if tag == hnode_e.Leaf:
468 node = cast(hnode.Leaf, UP_node)
469 f.PushColor(node.color)
470 f.write(j8_lite.EncodeString(node.s, unquoted_ok=True))
471 f.PopColor()
472
473 elif tag == hnode_e.External:
474 node = cast(hnode.External, UP_node)
475
476 f.PushColor(color_e.External)
477 if mylib.PYTHON:
478 f.write(repr(node.obj))
479 else:
480 f.write('UNTYPED any')
481 f.PopColor()
482
483 elif tag == hnode_e.Array:
484 node = cast(hnode.Array, UP_node)
485
486 # Can we fit the WHOLE array on the line?
487 f.write('[')
488 for i, item in enumerate(node.children):
489 if i != 0:
490 f.write(' ')
491 if not _TrySingleLine(item, f, max_chars):
492 return False
493 f.write(']')
494
495 elif tag == hnode_e.Record:
496 node = cast(hnode.Record, UP_node)
497
498 return _TrySingleLineObj(node, f, max_chars)
499
500 elif tag == hnode_e.AlreadySeen:
501 node = cast(hnode.AlreadySeen, UP_node)
502 # ... means omitting second reference, while --- means a cycle
503 f.write('...0x%s' % mylib.hex_lower(node.heap_id))
504
505 else:
506 raise AssertionError(hnode_str(node.tag()))
507
508 # Take into account the last char.
509 num_chars_so_far = f.NumChars()
510 if num_chars_so_far > max_chars:
511 return False
512
513 return True
514
515
516def PrintTree(node, f):
517 # type: (hnode_t, ColorOutput) -> None
518 pp = _PrettyPrinter(100) # max_col
519 pp.PrintNode(node, f, 0) # indent
520
521
522def PrintTree2(node, f):
523 # type: (hnode_t, ColorOutput) -> None
524 """
525 Make sure dependencies aren't a problem
526
527 TODO: asdl/pp_hnode.py, which is like display/pp_value.py
528 """
529 doc = pretty.AsciiText('foo')
530 printer = pretty.PrettyPrinter(20)
531
532 buf = mylib.BufWriter()
533 printer.PrintDoc(doc, buf)
534 f.write(buf.getvalue())
535 f.write('\n')