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

468 lines, 264 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 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 _YshDict(self, vdict):
329 # type: (value.Dict) -> MeasuredDoc
330 if len(vdict.d) == 0:
331 return UText("{}")
332 mdocs = [] # type: List[MeasuredDoc]
333 for k, v in iteritems(vdict.d):
334 mdocs.append(
335 _Concat([self._DictKey(k),
336 UText(": "),
337 self._Value(v)]))
338 return self._Surrounded("{", self._Join(mdocs, ",", " "), "}")
339
340 def _BashArray(self, varray):
341 # type: (value.BashArray) -> MeasuredDoc
342 type_name = self._Styled(self.type_style, UText("BashArray"))
343 if len(varray.strs) == 0:
344 return _Concat([UText("("), type_name, UText(")")])
345 mdocs = [] # type: List[MeasuredDoc]
346 for s in varray.strs:
347 if s is None:
348 mdocs.append(UText("null"))
349 else:
350 mdocs.append(self._BashStringLiteral(s))
351 return self._SurroundedAndPrefixed("(", type_name, " ",
352 self._Tabular(mdocs, ""), ")")
353
354 def _BashAssoc(self, vassoc):
355 # type: (value.BashAssoc) -> MeasuredDoc
356 type_name = self._Styled(self.type_style, UText("BashAssoc"))
357 if len(vassoc.d) == 0:
358 return _Concat([UText("("), type_name, UText(")")])
359 mdocs = [] # type: List[MeasuredDoc]
360 for k2, v2 in iteritems(vassoc.d):
361 mdocs.append(
362 _Concat([
363 UText("["),
364 self._BashStringLiteral(k2),
365 UText("]="),
366 self._BashStringLiteral(v2)
367 ]))
368 return self._SurroundedAndPrefixed("(", type_name, " ",
369 self._Join(mdocs, "", " "), ")")
370
371 def _SparseArray(self, val):
372 # type: (value.SparseArray) -> MeasuredDoc
373 type_name = self._Styled(self.type_style, UText("SparseArray"))
374 if len(val.d) == 0:
375 return _Concat([UText("("), type_name, UText(")")])
376 mdocs = [] # type: List[MeasuredDoc]
377 for k2, v2 in iteritems(val.d):
378 mdocs.append(
379 _Concat([
380 UText("["),
381 self._Styled(self.int_style, UText(mops.ToStr(k2))),
382 UText("]="),
383 self._BashStringLiteral(v2)
384 ]))
385 return self._SurroundedAndPrefixed("(", type_name, " ",
386 self._Join(mdocs, "", " "), ")")
387
388 def _Value(self, val):
389 # type: (value_t) -> MeasuredDoc
390
391 with tagswitch(val) as case:
392 if case(value_e.Null):
393 return self._Styled(self.null_style, UText("null"))
394
395 elif case(value_e.Bool):
396 b = cast(value.Bool, val).b
397 return self._Styled(self.bool_style,
398 UText("true" if b else "false"))
399
400 elif case(value_e.Int):
401 i = cast(value.Int, val).i
402 return self._Styled(self.int_style, UText(mops.ToStr(i)))
403
404 elif case(value_e.Float):
405 f = cast(value.Float, val).f
406 return self._Styled(self.float_style, UText(FloatString(f)))
407
408 elif case(value_e.Str):
409 s = cast(value.Str, val).s
410 return self._StringLiteral(s)
411
412 elif case(value_e.Range):
413 r = cast(value.Range, val)
414 type_name = self._Styled(self.type_style, UText(ValType(r)))
415 mdocs = [UText(str(r.lower)), UText(".."), UText(str(r.upper))]
416 return self._SurroundedAndPrefixed("(", type_name, " ",
417 self._Join(mdocs, "", " "),
418 ")")
419
420 elif case(value_e.List):
421 vlist = cast(value.List, val)
422 heap_id = j8.HeapValueId(vlist)
423 if self.visiting.get(heap_id, False):
424 return _Concat([
425 UText("["),
426 self._Styled(self.cycle_style, UText("...")),
427 UText("]")
428 ])
429 else:
430 self.visiting[heap_id] = True
431 result = self._YshList(vlist)
432 self.visiting[heap_id] = False
433 return result
434
435 elif case(value_e.Dict):
436 vdict = cast(value.Dict, val)
437 heap_id = j8.HeapValueId(vdict)
438 if self.visiting.get(heap_id, False):
439 return _Concat([
440 UText("{"),
441 self._Styled(self.cycle_style, UText("...")),
442 UText("}")
443 ])
444 else:
445 self.visiting[heap_id] = True
446 result = self._YshDict(vdict)
447 self.visiting[heap_id] = False
448 return result
449
450 elif case(value_e.SparseArray):
451 sparse = cast(value.SparseArray, val)
452 return self._SparseArray(sparse)
453
454 elif case(value_e.BashArray):
455 varray = cast(value.BashArray, val)
456 return self._BashArray(varray)
457
458 elif case(value_e.BashAssoc):
459 vassoc = cast(value.BashAssoc, val)
460 return self._BashAssoc(vassoc)
461
462 else:
463 type_name = self._Styled(self.type_style, UText(ValType(val)))
464 id_str = j8.ValueIdString(val)
465 return _Concat([UText("<"), type_name, UText(id_str + ">")])
466
467
468# vim: sw=4