OILS / display / pp_value.py View on Github | oilshell.org

499 lines, 289 significant
1#!/usr/bin/env python2
2"""
3Render Oils value_t -> doc_t, so it can be pretty printed
4"""
5
6from __future__ import print_function
7
8import math
9
10from _devbuild.gen.pretty_asdl import (doc, Measure, MeasuredDoc)
11from _devbuild.gen.value_asdl import Obj, value, value_e, value_t, value_str
12from data_lang import j8
13from data_lang import j8_lite
14from display.pretty import (_Break, _Concat, _Flat, _Group, _IfFlat, _Indent,
15 _EmptyMeasure)
16from display import ansi
17from frontend import match
18from mycpp import mops
19from mycpp.mylib import log, tagswitch, iteritems
20from typing import cast, List, Dict
21
22import libc
23
24_ = log
25
26
27def ValType(val):
28 # type: (value_t) -> str
29 """Returns a user-facing string like Int, Eggex, BashArray, etc."""
30 return value_str(val.tag(), dot=False)
31
32
33def FloatString(fl):
34 # type: (float) -> str
35
36 # Print in YSH syntax, similar to data_lang/j8.py
37 if math.isinf(fl):
38 s = 'INFINITY'
39 if fl < 0:
40 s = '-' + s
41 elif math.isnan(fl):
42 s = 'NAN'
43 else:
44 s = str(fl)
45 return s
46
47
48#
49# Unicode Helpers
50#
51
52
53def TryUnicodeWidth(s):
54 # type: (str) -> int
55 try:
56 width = libc.wcswidth(s)
57 except UnicodeError:
58 # e.g. en_US.UTF-8 locale missing, just return the number of bytes
59 width = len(s)
60
61 if width == -1: # non-printable wide char
62 return len(s)
63
64 return width
65
66
67def UText(string):
68 # type: (str) -> MeasuredDoc
69 """Print `string` (which must not contain a newline)."""
70 return MeasuredDoc(doc.Text(string), Measure(TryUnicodeWidth(string), -1))
71
72
73class ValueEncoder:
74 """Converts Oils values into `doc`s, which can then be pretty printed."""
75
76 def __init__(self):
77 # type: () -> None
78
79 # Default values
80 self.indent = 4
81 self.use_styles = True
82 # Tuned for 'data_lang/pretty-benchmark.sh float-demo'
83 # TODO: might want options for float width
84 self.max_tabular_width = 22
85
86 self.ysh_style = True
87
88 self.visiting = {} # type: Dict[int, bool]
89
90 # These can be configurable later
91 self.int_style = ansi.YELLOW
92 self.float_style = ansi.BLUE
93 self.null_style = ansi.RED
94 self.bool_style = ansi.CYAN
95 self.string_style = ansi.GREEN
96 self.cycle_style = ansi.BOLD + ansi.BLUE
97 self.type_style = ansi.MAGENTA
98
99 def SetIndent(self, indent):
100 # type: (int) -> None
101 """Set the number of spaces per indent."""
102 self.indent = indent
103
104 def SetUseStyles(self, use_styles):
105 # type: (bool) -> None
106 """Print with ansi colors and styles, rather than plain text."""
107 self.use_styles = use_styles
108
109 def SetMaxTabularWidth(self, max_tabular_width):
110 # type: (int) -> None
111 """Set the maximum width that list elements can be, for them to be
112 vertically aligned."""
113 self.max_tabular_width = max_tabular_width
114
115 def TypePrefix(self, type_str):
116 # type: (str) -> List[MeasuredDoc]
117 """Return docs for type string "(List)", which may break afterward."""
118 type_name = self._Styled(self.type_style, UText(type_str))
119
120 n = len(type_str)
121 # Our maximum string is "Float"
122 assert n <= 5, type_str
123
124 # Start printing in column 8. Adjust to 6 because () takes 2 spaces.
125 spaces = ' ' * (6 - n)
126
127 mdocs = [UText("("), type_name, UText(")"), _Break(spaces)]
128 return mdocs
129
130 def Value(self, val):
131 # type: (value_t) -> MeasuredDoc
132 """Convert an Oils value into a `doc`, which can then be pretty printed."""
133 self.visiting.clear()
134 return self._Value(val)
135
136 def _Styled(self, style, mdoc):
137 # type: (str, MeasuredDoc) -> MeasuredDoc
138 """Apply the ANSI style string to the given node, if use_styles is set."""
139 if self.use_styles:
140 return _Concat([
141 MeasuredDoc(doc.Text(style), _EmptyMeasure()), mdoc,
142 MeasuredDoc(doc.Text(ansi.RESET), _EmptyMeasure())
143 ])
144 else:
145 return mdoc
146
147 def _Surrounded(self, open, mdoc, close):
148 # type: (str, MeasuredDoc, str) -> MeasuredDoc
149 """Print one of two options (using '[', ']' for open, close):
150
151 ```
152 [mdoc]
153 ------
154 [
155 mdoc
156 ]
157 ```
158 """
159 return _Group(
160 _Concat([
161 UText(open),
162 _Indent(self.indent, _Concat([_Break(""), mdoc])),
163 _Break(""),
164 UText(close)
165 ]))
166
167 def _SurroundedAndPrefixed(self, open, prefix, sep, mdoc, close):
168 # type: (str, MeasuredDoc, str, MeasuredDoc, str) -> MeasuredDoc
169 """Print one of two options
170 (using '[', 'prefix', ':', 'mdoc', ']' for open, prefix, sep, mdoc, close):
171
172 ```
173 [prefix:mdoc]
174 ------
175 [prefix
176 mdoc
177 ]
178 ```
179 """
180 return _Group(
181 _Concat([
182 UText(open), prefix,
183 _Indent(self.indent, _Concat([_Break(sep), mdoc])),
184 _Break(""),
185 UText(close)
186 ]))
187
188 def _Join(self, items, sep, space):
189 # type: (List[MeasuredDoc], str, str) -> MeasuredDoc
190 """Join `items`, using either 'sep+space' or 'sep+newline' between them.
191
192 E.g., if sep and space are ',' and '_', print one of these two cases:
193 ```
194 first,_second,_third
195 ------
196 first,
197 second,
198 third
199 ```
200 """
201 seq = [] # type: List[MeasuredDoc]
202 for i, item in enumerate(items):
203 if i != 0:
204 seq.append(UText(sep))
205 seq.append(_Break(space))
206 seq.append(item)
207 return _Concat(seq)
208
209 def _Tabular(self, items, sep):
210 # type: (List[MeasuredDoc], str) -> MeasuredDoc
211 """Join `items` together, using one of three styles:
212
213 (showing spaces as underscores for clarity)
214 ```
215 first,_second,_third,_fourth,_fifth,_sixth,_seventh,_eighth
216 ------
217 first,___second,__third,
218 fourth,__fifth,___sixth,
219 seventh,_eighth
220 ------
221 first,
222 second,
223 third,
224 fourth,
225 fifth,
226 sixth,
227 seventh,
228 eighth
229 ```
230
231 The first "single line" style is used if the items fit on one line. The
232 second "tabular' style is used if the flat width of all items is no
233 greater than `self.max_tabular_width`. The third "multi line" style is
234 used otherwise.
235 """
236
237 # Why not "just" use tabular alignment so long as two items fit on every
238 # line? Because it isn't possible to check for that in the pretty
239 # printing language. There are two sorts of conditionals we can do:
240 #
241 # A. Inside the pretty printing language, which supports exactly one
242 # conditional: "does it fit on one line?".
243 # B. Outside the pretty printing language we can run arbitrary Python
244 # code, but we don't know how much space is available on the line
245 # because it depends on the context in which we're printed, which may
246 # vary.
247 #
248 # We're picking between the three styles, by using (A) to check if the
249 # first style fits on one line, then using (B) with "are all the items
250 # smaller than `self.max_tabular_width`?" to pick between style 2 and
251 # style 3.
252
253 if len(items) == 0:
254 return UText("")
255
256 max_flat_len = 0
257 seq = [] # type: List[MeasuredDoc]
258 for i, item in enumerate(items):
259 if i != 0:
260 seq.append(UText(sep))
261 seq.append(_Break(" "))
262 seq.append(item)
263 max_flat_len = max(max_flat_len, item.measure.flat)
264 non_tabular = _Concat(seq)
265
266 sep_width = TryUnicodeWidth(sep)
267 if max_flat_len + sep_width + 1 <= self.max_tabular_width:
268 tabular_seq = [] # type: List[MeasuredDoc]
269 for i, item in enumerate(items):
270 tabular_seq.append(_Flat(item))
271 if i != len(items) - 1:
272 padding = max_flat_len - item.measure.flat + 1
273 tabular_seq.append(UText(sep))
274 tabular_seq.append(_Group(_Break(" " * padding)))
275 tabular = _Concat(tabular_seq)
276 return _Group(_IfFlat(non_tabular, tabular))
277 else:
278 return non_tabular
279
280 def _DictKey(self, s):
281 # type: (str) -> MeasuredDoc
282 if match.IsValidVarName(s):
283 encoded = s
284 else:
285 if self.ysh_style:
286 encoded = j8_lite.YshEncodeString(s)
287 else:
288 # TODO: remove this dead branch after fixing tests
289 encoded = j8_lite.EncodeString(s)
290 return UText(encoded)
291
292 def _StringLiteral(self, s):
293 # type: (str) -> MeasuredDoc
294 if self.ysh_style:
295 # YSH r'' or b'' style
296 encoded = j8_lite.YshEncodeString(s)
297 else:
298 # TODO: remove this dead branch after fixing tests
299 encoded = j8_lite.EncodeString(s)
300 return self._Styled(self.string_style, UText(encoded))
301
302 def _BashStringLiteral(self, s):
303 # type: (str) -> MeasuredDoc
304
305 # '' or $'' style
306 #
307 # We mimic bash syntax by using $'\\' instead of b'\\'
308 #
309 # $ declare -a array=($'\\')
310 # $ = array
311 # (BashArray) (BashArray $'\\')
312 #
313 # $ declare -A assoc=([k]=$'\\')
314 # $ = assoc
315 # (BashAssoc) (BashAssoc ['k']=$'\\')
316
317 encoded = j8_lite.ShellEncode(s)
318 return self._Styled(self.string_style, UText(encoded))
319
320 def _YshList(self, vlist):
321 # type: (value.List) -> MeasuredDoc
322 """Print a string literal."""
323 if len(vlist.items) == 0:
324 return UText("[]")
325 mdocs = [self._Value(item) for item in vlist.items]
326 return self._Surrounded("[", self._Tabular(mdocs, ","), "]")
327
328 def _RawDict(self, d, open, close):
329 # type: (Dict[str, value_t], str, str) -> MeasuredDoc
330 mdocs = [] # type: List[MeasuredDoc]
331 for k, v in iteritems(d):
332 mdocs.append(
333 _Concat([self._DictKey(k),
334 UText(": "),
335 self._Value(v)]))
336 return self._Surrounded(open, self._Join(mdocs, ",", " "), close)
337
338 def _YshDict(self, vdict):
339 # type: (value.Dict) -> MeasuredDoc
340 if len(vdict.d) == 0:
341 return UText("{}")
342 return self._RawDict(vdict.d, "{", "}")
343
344 def _BashArray(self, varray):
345 # type: (value.BashArray) -> MeasuredDoc
346 type_name = self._Styled(self.type_style, UText("BashArray"))
347 if len(varray.strs) == 0:
348 return _Concat([UText("("), type_name, UText(")")])
349 mdocs = [] # type: List[MeasuredDoc]
350 for s in varray.strs:
351 if s is None:
352 mdocs.append(UText("null"))
353 else:
354 mdocs.append(self._BashStringLiteral(s))
355 return self._SurroundedAndPrefixed("(", type_name, " ",
356 self._Tabular(mdocs, ""), ")")
357
358 def _BashAssoc(self, vassoc):
359 # type: (value.BashAssoc) -> MeasuredDoc
360 type_name = self._Styled(self.type_style, UText("BashAssoc"))
361 if len(vassoc.d) == 0:
362 return _Concat([UText("("), type_name, UText(")")])
363 mdocs = [] # type: List[MeasuredDoc]
364 for k2, v2 in iteritems(vassoc.d):
365 mdocs.append(
366 _Concat([
367 UText("["),
368 self._BashStringLiteral(k2),
369 UText("]="),
370 self._BashStringLiteral(v2)
371 ]))
372 return self._SurroundedAndPrefixed("(", type_name, " ",
373 self._Join(mdocs, "", " "), ")")
374
375 def _SparseArray(self, val):
376 # type: (value.SparseArray) -> MeasuredDoc
377 type_name = self._Styled(self.type_style, UText("SparseArray"))
378 if len(val.d) == 0:
379 return _Concat([UText("("), type_name, UText(")")])
380 mdocs = [] # type: List[MeasuredDoc]
381 for k2, v2 in iteritems(val.d):
382 mdocs.append(
383 _Concat([
384 UText("["),
385 self._Styled(self.int_style, UText(mops.ToStr(k2))),
386 UText("]="),
387 self._BashStringLiteral(v2)
388 ]))
389 return self._SurroundedAndPrefixed("(", type_name, " ",
390 self._Join(mdocs, "", " "), ")")
391
392 def _Obj(self, obj):
393 # type: (Obj) -> MeasuredDoc
394 chain = [] # type: List[MeasuredDoc]
395 cur = obj
396 while cur is not None:
397 chain.append(self._RawDict(cur.d, "(", ")"))
398 cur = cur.prototype
399 if cur is not None:
400 chain.append(UText(" --> "))
401
402 return _Concat(chain)
403
404 def _Value(self, val):
405 # type: (value_t) -> MeasuredDoc
406
407 with tagswitch(val) as case:
408 if case(value_e.Null):
409 return self._Styled(self.null_style, UText("null"))
410
411 elif case(value_e.Bool):
412 b = cast(value.Bool, val).b
413 return self._Styled(self.bool_style,
414 UText("true" if b else "false"))
415
416 elif case(value_e.Int):
417 i = cast(value.Int, val).i
418 return self._Styled(self.int_style, UText(mops.ToStr(i)))
419
420 elif case(value_e.Float):
421 f = cast(value.Float, val).f
422 return self._Styled(self.float_style, UText(FloatString(f)))
423
424 elif case(value_e.Str):
425 s = cast(value.Str, val).s
426 return self._StringLiteral(s)
427
428 elif case(value_e.Range):
429 r = cast(value.Range, val)
430 type_name = self._Styled(self.type_style, UText(ValType(r)))
431 mdocs = [UText(str(r.lower)), UText("..<"), UText(str(r.upper))]
432 return self._SurroundedAndPrefixed("(", type_name, " ",
433 self._Join(mdocs, "", " "),
434 ")")
435
436 elif case(value_e.List):
437 vlist = cast(value.List, val)
438 heap_id = j8.HeapValueId(vlist)
439 if self.visiting.get(heap_id, False):
440 return _Concat([
441 UText("["),
442 self._Styled(self.cycle_style, UText("...")),
443 UText("]")
444 ])
445 else:
446 self.visiting[heap_id] = True
447 result = self._YshList(vlist)
448 self.visiting[heap_id] = False
449 return result
450
451 elif case(value_e.Dict):
452 vdict = cast(value.Dict, val)
453 heap_id = j8.HeapValueId(vdict)
454 if self.visiting.get(heap_id, False):
455 return _Concat([
456 UText("{"),
457 self._Styled(self.cycle_style, UText("...")),
458 UText("}")
459 ])
460 else:
461 self.visiting[heap_id] = True
462 result = self._YshDict(vdict)
463 self.visiting[heap_id] = False
464 return result
465
466 elif case(value_e.SparseArray):
467 sparse = cast(value.SparseArray, val)
468 return self._SparseArray(sparse)
469
470 elif case(value_e.BashArray):
471 varray = cast(value.BashArray, val)
472 return self._BashArray(varray)
473
474 elif case(value_e.BashAssoc):
475 vassoc = cast(value.BashAssoc, val)
476 return self._BashAssoc(vassoc)
477
478 elif case(value_e.Obj):
479 vaobj = cast(Obj, val)
480 heap_id = j8.HeapValueId(vaobj)
481 if self.visiting.get(heap_id, False):
482 return _Concat([
483 UText("("),
484 self._Styled(self.cycle_style, UText("...")),
485 UText(")")
486 ])
487 else:
488 self.visiting[heap_id] = True
489 result = self._Obj(vaobj)
490 self.visiting[heap_id] = False
491 return result
492
493 else:
494 type_name = self._Styled(self.type_style, UText(ValType(val)))
495 id_str = j8.ValueIdString(val)
496 return _Concat([UText("<"), type_name, UText(id_str + ">")])
497
498
499# vim: sw=4