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

513 lines, 299 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 _DictMdocs(self, d):
329 # type: (Dict[str, value_t]) -> List[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 mdocs
337
338 def _YshDict(self, vdict):
339 # type: (value.Dict) -> MeasuredDoc
340 if len(vdict.d) == 0:
341 return UText("{}")
342 mdocs = self._DictMdocs(vdict.d)
343 return self._Surrounded("{", self._Join(mdocs, ",", " "), "}")
344
345 def _BashArray(self, varray):
346 # type: (value.BashArray) -> MeasuredDoc
347 type_name = self._Styled(self.type_style, UText("BashArray"))
348 if len(varray.strs) == 0:
349 return _Concat([UText("("), type_name, UText(")")])
350 mdocs = [] # type: List[MeasuredDoc]
351 for s in varray.strs:
352 if s is None:
353 mdocs.append(UText("null"))
354 else:
355 mdocs.append(self._BashStringLiteral(s))
356 return self._SurroundedAndPrefixed("(", type_name, " ",
357 self._Tabular(mdocs, ""), ")")
358
359 def _BashAssoc(self, vassoc):
360 # type: (value.BashAssoc) -> MeasuredDoc
361 type_name = self._Styled(self.type_style, UText("BashAssoc"))
362 if len(vassoc.d) == 0:
363 return _Concat([UText("("), type_name, UText(")")])
364 mdocs = [] # type: List[MeasuredDoc]
365 for k2, v2 in iteritems(vassoc.d):
366 mdocs.append(
367 _Concat([
368 UText("["),
369 self._BashStringLiteral(k2),
370 UText("]="),
371 self._BashStringLiteral(v2)
372 ]))
373 return self._SurroundedAndPrefixed("(", type_name, " ",
374 self._Join(mdocs, "", " "), ")")
375
376 def _SparseArray(self, val):
377 # type: (value.SparseArray) -> MeasuredDoc
378 type_name = self._Styled(self.type_style, UText("SparseArray"))
379 if len(val.d) == 0:
380 return _Concat([UText("("), type_name, UText(")")])
381 mdocs = [] # type: List[MeasuredDoc]
382 for k2, v2 in iteritems(val.d):
383 mdocs.append(
384 _Concat([
385 UText("["),
386 self._Styled(self.int_style, UText(mops.ToStr(k2))),
387 UText("]="),
388 self._BashStringLiteral(v2)
389 ]))
390 return self._SurroundedAndPrefixed("(", type_name, " ",
391 self._Join(mdocs, "", " "), ")")
392
393 def _Obj(self, obj):
394 # type: (Obj) -> MeasuredDoc
395 chain = [] # type: List[MeasuredDoc]
396 cur = obj
397 while cur is not None:
398 mdocs = self._DictMdocs(cur.d)
399 chain.append(
400 self._Surrounded("(", self._Join(mdocs, ",", " "), ")"))
401 cur = cur.prototype
402 if cur is not None:
403 chain.append(UText(" --> "))
404
405 return _Concat(chain)
406
407 def _Value(self, val):
408 # type: (value_t) -> MeasuredDoc
409
410 with tagswitch(val) as case:
411 if case(value_e.Null):
412 return self._Styled(self.null_style, UText("null"))
413
414 elif case(value_e.Bool):
415 b = cast(value.Bool, val).b
416 return self._Styled(self.bool_style,
417 UText("true" if b else "false"))
418
419 elif case(value_e.Int):
420 i = cast(value.Int, val).i
421 return self._Styled(self.int_style, UText(mops.ToStr(i)))
422
423 elif case(value_e.Float):
424 f = cast(value.Float, val).f
425 return self._Styled(self.float_style, UText(FloatString(f)))
426
427 elif case(value_e.Str):
428 s = cast(value.Str, val).s
429 return self._StringLiteral(s)
430
431 elif case(value_e.Range):
432 r = cast(value.Range, val)
433 type_name = self._Styled(self.type_style, UText(ValType(r)))
434 mdocs = [
435 UText(str(r.lower)),
436 UText("..<"),
437 UText(str(r.upper))
438 ]
439 return self._SurroundedAndPrefixed("(", type_name, " ",
440 self._Join(mdocs, "", " "),
441 ")")
442
443 elif case(value_e.List):
444 vlist = cast(value.List, val)
445 heap_id = j8.HeapValueId(vlist)
446 if self.visiting.get(heap_id, False):
447 return _Concat([
448 UText("["),
449 self._Styled(self.cycle_style, UText("...")),
450 UText("]")
451 ])
452 else:
453 self.visiting[heap_id] = True
454 result = self._YshList(vlist)
455 self.visiting[heap_id] = False
456 return result
457
458 elif case(value_e.Dict):
459 vdict = cast(value.Dict, val)
460 heap_id = j8.HeapValueId(vdict)
461 if self.visiting.get(heap_id, False):
462 return _Concat([
463 UText("{"),
464 self._Styled(self.cycle_style, UText("...")),
465 UText("}")
466 ])
467 else:
468 self.visiting[heap_id] = True
469 result = self._YshDict(vdict)
470 self.visiting[heap_id] = False
471 return result
472
473 elif case(value_e.SparseArray):
474 sparse = cast(value.SparseArray, val)
475 return self._SparseArray(sparse)
476
477 elif case(value_e.BashArray):
478 varray = cast(value.BashArray, val)
479 return self._BashArray(varray)
480
481 elif case(value_e.BashAssoc):
482 vassoc = cast(value.BashAssoc, val)
483 return self._BashAssoc(vassoc)
484
485 elif case(value_e.Obj):
486 vaobj = cast(Obj, val)
487 heap_id = j8.HeapValueId(vaobj)
488 if self.visiting.get(heap_id, False):
489 return _Concat([
490 UText("("),
491 self._Styled(self.cycle_style, UText("...")),
492 UText(")")
493 ])
494 else:
495 self.visiting[heap_id] = True
496 result = self._Obj(vaobj)
497 self.visiting[heap_id] = False
498 return result
499
500 # Bug fix: these types are GLOBAL singletons in C++. This means
501 # they have no object ID, so j8.ValueIdString() will CRASH on them.
502
503 elif case(value_e.Stdin, value_e.Interrupted):
504 type_name = self._Styled(self.type_style, UText(ValType(val)))
505 return _Concat([UText("<"), type_name, UText(">")])
506
507 else:
508 type_name = self._Styled(self.type_style, UText(ValType(val)))
509 id_str = j8.ValueIdString(val)
510 return _Concat([UText("<"), type_name, UText(id_str + ">")])
511
512
513# vim: sw=4