1 | """
|
2 | format.py -- Pretty print an ASDL data structure.
|
3 |
|
4 | Like encode.py, but uses text instead of binary.
|
5 |
|
6 | TODO:
|
7 |
|
8 | - auto-abbreviation of single field things (minus location)
|
9 |
|
10 | - option to omit spaces for SQ, SQ, W? It's all one thing.
|
11 |
|
12 | Places where we try a single line:
|
13 | - arrays
|
14 | - objects with name fields
|
15 | - abbreviated, unnamed fields
|
16 | """
|
17 |
|
18 | import re
|
19 |
|
20 | from asdl import asdl_ as asdl
|
21 | from core import util
|
22 |
|
23 | import os
|
24 | if not os.getenv('_OVM_DEPS'):
|
25 | import cgi
|
26 |
|
27 |
|
28 | def DetectConsoleOutput(f):
|
29 | """Wrapped to auto-detect."""
|
30 | if f.isatty():
|
31 | return AnsiOutput(f)
|
32 | else:
|
33 | return TextOutput(f)
|
34 |
|
35 |
|
36 | class ColorOutput(object):
|
37 | """Abstract base class for plain text, ANSI color, and HTML color."""
|
38 |
|
39 | def __init__(self, f):
|
40 | self.f = f
|
41 | self.num_chars = 0
|
42 |
|
43 | def NewTempBuffer(self):
|
44 | """Return a temporary buffer for the line wrapping calculation."""
|
45 | raise NotImplementedError
|
46 |
|
47 | def FileHeader(self):
|
48 | """Hook for printing a full file."""
|
49 | pass
|
50 |
|
51 | def FileFooter(self):
|
52 | """Hook for printing a full file."""
|
53 | pass
|
54 |
|
55 | def PushColor(self, str_type):
|
56 | raise NotImplementedError
|
57 |
|
58 | def PopColor(self):
|
59 | raise NotImplementedError
|
60 |
|
61 | def write(self, s):
|
62 | self.f.write(s)
|
63 | self.num_chars += len(s) # Only count visible characters!
|
64 |
|
65 | def WriteRaw(self, raw):
|
66 | """
|
67 | Write raw data without escaping, and without counting control codes in the
|
68 | length.
|
69 | """
|
70 | s, num_chars = raw
|
71 | self.f.write(s)
|
72 | self.num_chars += num_chars
|
73 |
|
74 | def NumChars(self):
|
75 | return self.num_chars
|
76 |
|
77 | def GetRaw(self):
|
78 | # For when we have an io.StringIO()
|
79 | return self.f.getvalue(), self.num_chars
|
80 |
|
81 |
|
82 | class TextOutput(ColorOutput):
|
83 | """TextOutput put obeys the color interface, but outputs nothing."""
|
84 |
|
85 | def __init__(self, f):
|
86 | ColorOutput.__init__(self, f)
|
87 |
|
88 | def NewTempBuffer(self):
|
89 | return TextOutput(util.Buffer())
|
90 |
|
91 | def PushColor(self, str_type):
|
92 | pass # ignore color
|
93 |
|
94 | def PopColor(self):
|
95 | pass # ignore color
|
96 |
|
97 |
|
98 | class HtmlOutput(ColorOutput):
|
99 | """
|
100 | HTML one can have wider columns. Maybe not even fixed-width font. Hm yeah
|
101 | indentation should be logical then?
|
102 |
|
103 | Color: HTML spans
|
104 | """
|
105 | def __init__(self, f):
|
106 | ColorOutput.__init__(self, f)
|
107 |
|
108 | def NewTempBuffer(self):
|
109 | return HtmlOutput(util.Buffer())
|
110 |
|
111 | def FileHeader(self):
|
112 | # TODO: Use a different CSS file to make the colors match. I like string
|
113 | # literals as yellow, etc.
|
114 | #<link rel="stylesheet" type="text/css" href="/css/code.css" />
|
115 | self.f.write("""
|
116 | <html>
|
117 | <head>
|
118 | <title>oil AST</title>
|
119 | <style>
|
120 | .n { color: brown }
|
121 | .s { font-weight: bold }
|
122 | .o { color: darkgreen }
|
123 | </style>
|
124 | </head>
|
125 | <body>
|
126 | <pre>
|
127 | """)
|
128 |
|
129 | def FileFooter(self):
|
130 | self.f.write("""
|
131 | </pre>
|
132 | </body>
|
133 | </html>
|
134 | """)
|
135 |
|
136 | def PushColor(self, str_type):
|
137 | # To save bandwidth, use single character CSS names.
|
138 | if str_type == _NODE_TYPE:
|
139 | css_class = 'n'
|
140 | elif str_type == _STRING_LITERAL:
|
141 | css_class = 's'
|
142 | elif str_type == _OTHER_LITERAL:
|
143 | css_class = 'o'
|
144 | elif str_type == _OTHER_TYPE:
|
145 | css_class = 'o'
|
146 | else:
|
147 | raise AssertionError(str_type)
|
148 | self.f.write('<span class="%s">' % css_class)
|
149 |
|
150 | def PopColor(self):
|
151 | self.f.write('</span>')
|
152 |
|
153 | def write(self, s):
|
154 | # PROBLEM: Double escaping!
|
155 | self.f.write(cgi.escape(s))
|
156 | self.num_chars += len(s) # Only count visible characters!
|
157 |
|
158 |
|
159 | # Color token types
|
160 | _NODE_TYPE = 1
|
161 | _STRING_LITERAL = 2
|
162 | _OTHER_LITERAL = 3 # Int and bool. Green?
|
163 | _OTHER_TYPE = 4 # Or
|
164 |
|
165 |
|
166 | # ANSI color constants (also in sh_spec.py)
|
167 | _RESET = '\033[0;0m'
|
168 | _BOLD = '\033[1m'
|
169 |
|
170 | _RED = '\033[31m'
|
171 | _GREEN = '\033[32m'
|
172 | _BLUE = '\033[34m'
|
173 |
|
174 | _YELLOW = '\033[33m'
|
175 | _CYAN = '\033[36m'
|
176 |
|
177 |
|
178 | class AnsiOutput(ColorOutput):
|
179 | """For the console."""
|
180 |
|
181 | def __init__(self, f):
|
182 | ColorOutput.__init__(self, f)
|
183 |
|
184 | def NewTempBuffer(self):
|
185 | return AnsiOutput(util.Buffer())
|
186 |
|
187 | def PushColor(self, str_type):
|
188 | if str_type == _NODE_TYPE:
|
189 | #self.f.write(_GREEN)
|
190 | self.f.write(_YELLOW)
|
191 | elif str_type == _STRING_LITERAL:
|
192 | self.f.write(_BOLD)
|
193 | elif str_type == _OTHER_LITERAL:
|
194 | self.f.write(_GREEN)
|
195 | elif str_type == _OTHER_TYPE:
|
196 | self.f.write(_GREEN) # Same color as other literals for now
|
197 | else:
|
198 | raise AssertionError(str_type)
|
199 |
|
200 | def PopColor(self):
|
201 | self.f.write(_RESET)
|
202 |
|
203 |
|
204 | #
|
205 | # Nodes
|
206 | #
|
207 |
|
208 |
|
209 | class _Obj(object):
|
210 | """Node for pretty-printing."""
|
211 | def __init__(self, node_type):
|
212 | self.node_type = node_type
|
213 | self.fields = [] # list of 2-tuples of (name, Obj or ColoredString)
|
214 |
|
215 | # Custom hooks can change these:
|
216 | self.abbrev = False
|
217 | self.show_node_type = True # only respected when abbrev is false
|
218 | self.left = '('
|
219 | self.right = ')'
|
220 | self.unnamed_fields = [] # if this is set, it's printed instead?
|
221 | # problem: CompoundWord just has word_part though
|
222 | # List of Obj or ColoredString
|
223 |
|
224 | def __repr__(self):
|
225 | return '<_Obj %s %s>' % (self.node_type, self.fields)
|
226 |
|
227 |
|
228 | class _ColoredString(object):
|
229 | """Node for pretty-printing."""
|
230 | def __init__(self, s, str_type):
|
231 | assert isinstance(s, str), s
|
232 | self.s = s
|
233 | self.str_type = str_type
|
234 |
|
235 | def __repr__(self):
|
236 | return '<_ColoredString %s %s>' % (self.s, self.str_type)
|
237 |
|
238 |
|
239 | def MakeFieldSubtree(obj, field_name, desc, abbrev_hook, omit_empty=True):
|
240 | try:
|
241 | field_val = getattr(obj, field_name)
|
242 | except AttributeError:
|
243 | # This happens when required fields are not initialized, e.g. FuncCall()
|
244 | # without setting name.
|
245 | raise AssertionError(
|
246 | '%s is missing field %r' % (obj.__class__, field_name))
|
247 |
|
248 | if isinstance(desc, asdl.IntType):
|
249 | out_val = _ColoredString(str(field_val), _OTHER_LITERAL)
|
250 |
|
251 | elif isinstance(desc, asdl.BoolType):
|
252 | out_val = _ColoredString('T' if field_val else 'F', _OTHER_LITERAL)
|
253 |
|
254 | elif isinstance(desc, asdl.Sum) and asdl.is_simple(desc):
|
255 | out_val = field_val.name
|
256 |
|
257 | elif isinstance(desc, asdl.StrType):
|
258 | out_val = _ColoredString(field_val, _STRING_LITERAL)
|
259 |
|
260 | elif isinstance(desc, asdl.ArrayType):
|
261 | out_val = []
|
262 | obj_list = field_val
|
263 | for child_obj in obj_list:
|
264 | t = MakeTree(child_obj, abbrev_hook)
|
265 | out_val.append(t)
|
266 |
|
267 | if omit_empty and not obj_list:
|
268 | out_val = None
|
269 |
|
270 | elif isinstance(desc, asdl.MaybeType):
|
271 | if field_val is None:
|
272 | out_val = None
|
273 | else:
|
274 | out_val = MakeTree(field_val, abbrev_hook)
|
275 |
|
276 | else:
|
277 | out_val = MakeTree(field_val, abbrev_hook)
|
278 |
|
279 | return out_val
|
280 |
|
281 |
|
282 | def MakeTree(obj, abbrev_hook=None, omit_empty=True):
|
283 | """The first step of printing: create a homogeneous tree.
|
284 |
|
285 | Args:
|
286 | obj: py_meta.Obj
|
287 | omit_empty: Whether to omit empty lists
|
288 | Returns:
|
289 | _Obj node
|
290 | """
|
291 | from asdl import py_meta
|
292 |
|
293 | if isinstance(obj, py_meta.SimpleObj): # Primitive
|
294 | return obj.name
|
295 |
|
296 | elif isinstance(obj, py_meta.CompoundObj):
|
297 | # These lines can be possibly COMBINED all into one. () can replace
|
298 | # indentation?
|
299 | out_node = _Obj(obj.__class__.__name__)
|
300 |
|
301 | for field_name, desc in obj.ASDL_TYPE.GetFields():
|
302 | out_val = MakeFieldSubtree(obj, field_name, desc, abbrev_hook,
|
303 | omit_empty=omit_empty)
|
304 |
|
305 | if out_val is not None:
|
306 | out_node.fields.append((field_name, out_val))
|
307 |
|
308 | # Call user-defined hook to abbreviate compound objects.
|
309 | if abbrev_hook:
|
310 | abbrev_hook(obj, out_node)
|
311 |
|
312 | elif isinstance(obj, str): # Could be an array of strings
|
313 | return _ColoredString(obj, _STRING_LITERAL)
|
314 |
|
315 | else:
|
316 | # Id uses this now. TODO: Should we have plugins? Might need it for
|
317 | # color.
|
318 | return _ColoredString(repr(obj), _OTHER_TYPE)
|
319 |
|
320 | return out_node
|
321 |
|
322 |
|
323 | # This is word characters, - and _, as well as path name characters . and /.
|
324 | _PLAIN_RE = re.compile(r'^[a-zA-Z0-9\-_./]+$')
|
325 |
|
326 | # NOTE: Turning JSON back on can be a cheap hack for detecting invalid unicode.
|
327 | # But we want to write our own AST walker for that.
|
328 |
|
329 | def _PrettyString(s):
|
330 | if '\n' in s:
|
331 | #return json.dumps(s) # account for the fact that $ matches the newline
|
332 | return repr(s)
|
333 | if _PLAIN_RE.match(s):
|
334 | return s
|
335 | else:
|
336 | #return json.dumps(s)
|
337 | return repr(s)
|
338 |
|
339 |
|
340 | INDENT = 2
|
341 |
|
342 | def _PrintWrappedArray(array, prefix_len, f, indent, max_col):
|
343 | """Print an array of objects with line wrapping.
|
344 |
|
345 | Returns whether they all fit on a single line, so you can print the closing
|
346 | brace properly.
|
347 | """
|
348 | all_fit = True
|
349 | chars_so_far = prefix_len
|
350 |
|
351 | for i, val in enumerate(array):
|
352 | if i != 0:
|
353 | f.write(' ')
|
354 |
|
355 | single_f = f.NewTempBuffer()
|
356 | if _TrySingleLine(val, single_f, max_col - chars_so_far):
|
357 | f.WriteRaw(single_f.GetRaw())
|
358 | chars_so_far += single_f.NumChars()
|
359 | else: # WRAP THE LINE
|
360 | f.write('\n')
|
361 | # TODO: Add max_col here, taking into account the field name
|
362 | new_indent = indent + INDENT
|
363 | PrintTree(val, f, indent=new_indent, max_col=max_col)
|
364 |
|
365 | chars_so_far = 0 # allow more
|
366 | all_fit = False
|
367 | return all_fit
|
368 |
|
369 |
|
370 | def _PrintWholeArray(array, prefix_len, f, indent, max_col):
|
371 | # This is UNLIKE the abbreviated case above, where we do WRAPPING.
|
372 | # Here, ALL children must fit on a single line, or else we separate
|
373 | # each one oonto a separate line. This is to avoid the following:
|
374 | #
|
375 | # children: [(C ...)
|
376 | # (C ...)
|
377 | # ]
|
378 | # The first child is out of line. The abbreviated objects have a
|
379 | # small header like C or DQ so it doesn't matter as much.
|
380 | all_fit = True
|
381 | pieces = []
|
382 | chars_so_far = prefix_len
|
383 | for item in array:
|
384 | single_f = f.NewTempBuffer()
|
385 | if _TrySingleLine(item, single_f, max_col - chars_so_far):
|
386 | pieces.append(single_f.GetRaw())
|
387 | chars_so_far += single_f.NumChars()
|
388 | else:
|
389 | all_fit = False
|
390 | break
|
391 |
|
392 | if all_fit:
|
393 | for i, p in enumerate(pieces):
|
394 | if i != 0:
|
395 | f.write(' ')
|
396 | f.WriteRaw(p)
|
397 | f.write(']')
|
398 | return all_fit
|
399 |
|
400 |
|
401 | def _PrintTreeObj(node, f, indent, max_col):
|
402 | """Print a CompoundObj in abbreviated or normal form."""
|
403 | ind = ' ' * indent
|
404 |
|
405 | if node.abbrev: # abbreviated
|
406 | prefix = ind + node.left
|
407 | f.write(prefix)
|
408 | if node.show_node_type:
|
409 | f.PushColor(_NODE_TYPE)
|
410 | f.write(node.node_type)
|
411 | f.PopColor()
|
412 | f.write(' ')
|
413 |
|
414 | prefix_len = len(prefix) + len(node.node_type) + 1
|
415 | all_fit = _PrintWrappedArray(
|
416 | node.unnamed_fields, prefix_len, f, indent, max_col)
|
417 |
|
418 | if not all_fit:
|
419 | f.write('\n')
|
420 | f.write(ind)
|
421 | f.write(node.right)
|
422 |
|
423 | else: # full form like (SimpleCommand ...)
|
424 | f.write(ind + node.left)
|
425 |
|
426 | f.PushColor(_NODE_TYPE)
|
427 | f.write(node.node_type)
|
428 | f.PopColor()
|
429 |
|
430 | f.write('\n')
|
431 | i = 0
|
432 | for name, val in node.fields:
|
433 | ind1 = ' ' * (indent+INDENT)
|
434 | if isinstance(val, list): # list field
|
435 | name_str = '%s%s: [' % (ind1, name)
|
436 | f.write(name_str)
|
437 | prefix_len = len(name_str)
|
438 |
|
439 | if not _PrintWholeArray(val, prefix_len, f, indent, max_col):
|
440 | f.write('\n')
|
441 | for child in val:
|
442 | # TODO: Add max_col here
|
443 | PrintTree(child, f, indent=indent+INDENT+INDENT)
|
444 | f.write('\n')
|
445 | f.write('%s]' % ind1)
|
446 |
|
447 | else: # primitive field
|
448 | name_str = '%s%s: ' % (ind1, name)
|
449 | f.write(name_str)
|
450 | prefix_len = len(name_str)
|
451 |
|
452 | # Try to print it on the same line as the field name; otherwise print
|
453 | # it on a separate line.
|
454 | single_f = f.NewTempBuffer()
|
455 | if _TrySingleLine(val, single_f, max_col - prefix_len):
|
456 | f.WriteRaw(single_f.GetRaw())
|
457 | else:
|
458 | f.write('\n')
|
459 | # TODO: Add max_col here, taking into account the field name
|
460 | PrintTree(val, f, indent=indent+INDENT+INDENT)
|
461 | i += 1
|
462 |
|
463 | f.write('\n') # separate fields
|
464 |
|
465 | f.write(ind + node.right)
|
466 |
|
467 |
|
468 | def PrintTree(node, f, indent=0, max_col=100):
|
469 | """Second step of printing: turn homogeneous tree into a colored string.
|
470 |
|
471 | Args:
|
472 | node: homogeneous tree node
|
473 | f: ColorOutput instance.
|
474 | max_col: don't print past this column number on ANY line
|
475 | """
|
476 | ind = ' ' * indent
|
477 |
|
478 | # Try printing on a single line
|
479 | single_f = f.NewTempBuffer()
|
480 | single_f.write(ind)
|
481 | if _TrySingleLine(node, single_f, max_col - indent):
|
482 | f.WriteRaw(single_f.GetRaw())
|
483 | return
|
484 |
|
485 | if isinstance(node, str):
|
486 | f.write(ind + _PrettyString(node))
|
487 |
|
488 | elif isinstance(node, _ColoredString):
|
489 | f.PushColor(node.str_type)
|
490 | f.write(_PrettyString(node.s))
|
491 | f.PopColor()
|
492 |
|
493 | elif isinstance(node, _Obj):
|
494 | _PrintTreeObj(node, f, indent, max_col)
|
495 |
|
496 | else:
|
497 | raise AssertionError(node)
|
498 |
|
499 |
|
500 | def _TrySingleLineObj(node, f, max_chars):
|
501 | """Print an object on a single line."""
|
502 | f.write(node.left)
|
503 | if node.abbrev:
|
504 | if node.show_node_type:
|
505 | f.PushColor(_NODE_TYPE)
|
506 | f.write(node.node_type)
|
507 | f.PopColor()
|
508 | f.write(' ')
|
509 |
|
510 | for i, val in enumerate(node.unnamed_fields):
|
511 | if i != 0:
|
512 | f.write(' ')
|
513 | if not _TrySingleLine(val, f, max_chars):
|
514 | return False
|
515 | else:
|
516 | f.PushColor(_NODE_TYPE)
|
517 | f.write(node.node_type)
|
518 | f.PopColor()
|
519 |
|
520 | for name, val in node.fields:
|
521 | f.write(' %s:' % name)
|
522 | if not _TrySingleLine(val, f, max_chars):
|
523 | return False
|
524 |
|
525 | f.write(node.right)
|
526 | return True
|
527 |
|
528 |
|
529 | def _TrySingleLine(node, f, max_chars):
|
530 | """Try printing on a single line.
|
531 |
|
532 | Args:
|
533 | node: homogeneous tree node
|
534 | f: ColorOutput instance
|
535 | max_chars: maximum number of characters to print on THIS line
|
536 | indent: current indent level
|
537 |
|
538 | Returns:
|
539 | ok: whether it fit on the line of the given size.
|
540 | If False, you can't use the value of f.
|
541 | """
|
542 | if isinstance(node, str):
|
543 | f.write(_PrettyString(node))
|
544 |
|
545 | elif isinstance(node, _ColoredString):
|
546 | f.PushColor(node.str_type)
|
547 | f.write(_PrettyString(node.s))
|
548 | f.PopColor()
|
549 |
|
550 | elif isinstance(node, list): # Can we fit the WHOLE list on the line?
|
551 | f.write('[')
|
552 | for i, item in enumerate(node):
|
553 | if i != 0:
|
554 | f.write(' ')
|
555 | if not _TrySingleLine(item, f, max_chars):
|
556 | return False
|
557 | f.write(']')
|
558 |
|
559 | elif isinstance(node, _Obj):
|
560 | return _TrySingleLineObj(node, f, max_chars)
|
561 |
|
562 | else:
|
563 | raise AssertionError("Unexpected node: %r (%r)" % (node, node.__class__))
|
564 |
|
565 | # Take into account the last char.
|
566 | num_chars_so_far = f.NumChars()
|
567 | if num_chars_so_far > max_chars:
|
568 | return False
|
569 |
|
570 | return True
|