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

501 lines, 291 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(self._Surrounded("(", self._Join(mdocs, ",", " "), ")"))
400 cur = cur.prototype
401 if cur is not None:
402 chain.append(UText(" --> "))
403
404 return _Concat(chain)
405
406 def _Value(self, val):
407 # type: (value_t) -> MeasuredDoc
408
409 with tagswitch(val) as case:
410 if case(value_e.Null):
411 return self._Styled(self.null_style, UText("null"))
412
413 elif case(value_e.Bool):
414 b = cast(value.Bool, val).b
415 return self._Styled(self.bool_style,
416 UText("true" if b else "false"))
417
418 elif case(value_e.Int):
419 i = cast(value.Int, val).i
420 return self._Styled(self.int_style, UText(mops.ToStr(i)))
421
422 elif case(value_e.Float):
423 f = cast(value.Float, val).f
424 return self._Styled(self.float_style, UText(FloatString(f)))
425
426 elif case(value_e.Str):
427 s = cast(value.Str, val).s
428 return self._StringLiteral(s)
429
430 elif case(value_e.Range):
431 r = cast(value.Range, val)
432 type_name = self._Styled(self.type_style, UText(ValType(r)))
433 mdocs = [UText(str(r.lower)), UText("..<"), UText(str(r.upper))]
434 return self._SurroundedAndPrefixed("(", type_name, " ",
435 self._Join(mdocs, "", " "),
436 ")")
437
438 elif case(value_e.List):
439 vlist = cast(value.List, val)
440 heap_id = j8.HeapValueId(vlist)
441 if self.visiting.get(heap_id, False):
442 return _Concat([
443 UText("["),
444 self._Styled(self.cycle_style, UText("...")),
445 UText("]")
446 ])
447 else:
448 self.visiting[heap_id] = True
449 result = self._YshList(vlist)
450 self.visiting[heap_id] = False
451 return result
452
453 elif case(value_e.Dict):
454 vdict = cast(value.Dict, val)
455 heap_id = j8.HeapValueId(vdict)
456 if self.visiting.get(heap_id, False):
457 return _Concat([
458 UText("{"),
459 self._Styled(self.cycle_style, UText("...")),
460 UText("}")
461 ])
462 else:
463 self.visiting[heap_id] = True
464 result = self._YshDict(vdict)
465 self.visiting[heap_id] = False
466 return result
467
468 elif case(value_e.SparseArray):
469 sparse = cast(value.SparseArray, val)
470 return self._SparseArray(sparse)
471
472 elif case(value_e.BashArray):
473 varray = cast(value.BashArray, val)
474 return self._BashArray(varray)
475
476 elif case(value_e.BashAssoc):
477 vassoc = cast(value.BashAssoc, val)
478 return self._BashAssoc(vassoc)
479
480 elif case(value_e.Obj):
481 vaobj = cast(Obj, val)
482 heap_id = j8.HeapValueId(vaobj)
483 if self.visiting.get(heap_id, False):
484 return _Concat([
485 UText("("),
486 self._Styled(self.cycle_style, UText("...")),
487 UText(")")
488 ])
489 else:
490 self.visiting[heap_id] = True
491 result = self._Obj(vaobj)
492 self.visiting[heap_id] = False
493 return result
494
495 else:
496 type_name = self._Styled(self.type_style, UText(ValType(val)))
497 id_str = j8.ValueIdString(val)
498 return _Concat([UText("<"), type_name, UText(id_str + ">")])
499
500
501# vim: sw=4