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

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