-
-
Notifications
You must be signed in to change notification settings - Fork 395
/
pprint.py
415 lines (348 loc) · 16 KB
/
pprint.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
"""
HoloViews can be used to build highly-nested data-structures
containing large amounts of raw data. As a result, it is difficult to
generate a readable representation that is both informative yet
concise.
As a result, HoloViews does not attempt to build representations that
can be evaluated with eval; such representations would typically be
far too large to be practical. Instead, all HoloViews objects can be
represented as tree structures, showing how to access and index into
your data.
"""
import re
import textwrap
import param
from param.ipython import ParamPager
from param.parameterized import bothmethod
from .util import group_sanitizer, label_sanitizer
class ParamFilter(param.ParameterizedFunction):
"""
Given a parameterized object, return a proxy parameterized object
holding only the parameters that match some filter criterion.
A filter is supplied with the parameter name and the parameter
object and must return a boolean. A regular expression filter has
been supplied and may be used to search for parameters mentioning
'bounds' as follows:
filtered = ParamFilter(obj, ParamFilter.regexp_filter('bounds'))
This may be used to filter documentation generated by param.
"""
def __call__(self, obj, filter_fn=None):
if filter_fn is None:
return obj
name = obj.__name__ if isinstance(obj,type) else obj.__class__.__name__
class_proxy = type(name, (param.Parameterized,),
{k:v for k,v in obj.param.objects('existing').items()
if filter_fn(k,v)})
if isinstance(obj,type):
return class_proxy
else:
instance_params = obj.param.values().items()
obj_proxy = class_proxy()
filtered = {k:v for k,v in instance_params
if (k in obj_proxy.param)
and not obj_proxy.param.objects('existing')[k].constant}
obj_proxy.param.update(**filtered)
return obj_proxy
@param.parameterized.bothmethod
def regexp_filter(self_or_cls, pattern):
"""
Builds a parameter filter using the supplied pattern (may be a
general Python regular expression)
"""
def inner_filter(name, p):
name_match = re.search(pattern,name)
if name_match is not None:
return True
if p.doc is not None:
doc_match = re.search(pattern,p.doc)
if doc_match is not None:
return True
return False
return inner_filter
class InfoPrinter:
"""
Class for printing other information related to an object that is
of use to the user.
"""
headings = ['\x1b[1;35m%s\x1b[0m', '\x1b[1;32m%s\x1b[0m']
ansi_escape = re.compile(r'\x1b[^m]*m')
ppager = ParamPager()
store = None
elements = []
@classmethod
def get_parameter_info(cls, obj, ansi=False, show_values=True,
pattern=None, max_col_len=40):
"""
Get parameter information from the supplied class or object.
"""
if cls.ppager is None: return ''
if pattern is not None:
obj = ParamFilter(obj, ParamFilter.regexp_filter(pattern))
if len(list(obj.param)) <= 1:
return None
param_info = cls.ppager.get_param_info(obj)
param_list = cls.ppager.param_docstrings(param_info)
if not show_values:
retval = cls.ansi_escape.sub('', param_list) if not ansi else param_list
return cls.highlight(pattern, retval)
else:
info = cls.ppager(obj)
if ansi is False:
info = cls.ansi_escape.sub('', info)
return cls.highlight(pattern, info)
@classmethod
def heading(cls, heading_text, char='=', level=0, ansi=False):
"""
Turn the supplied heading text into a suitable heading with
optional underline and color.
"""
heading_color = cls.headings[level] if ansi else '%s'
if char is None:
return heading_color % f'{heading_text}\n'
else:
heading_ul = char*len(heading_text)
return heading_color % f'{heading_ul}\n{heading_text}\n{heading_ul}'
@classmethod
def highlight(cls, pattern, string):
if pattern is None: return string
return re.sub(pattern, '\033[43;1;30m\\g<0>\x1b[0m',
string, flags=re.IGNORECASE)
@classmethod
def info(cls, obj, ansi=False, backend='matplotlib', visualization=True,
pattern=None, elements=None):
"""
Show information about an object in the given category. ANSI
color codes may be enabled or disabled.
"""
if elements is None:
elements = []
cls.elements = elements
ansi_escape = re.compile(r'\x1b[^m]*m')
isclass = isinstance(obj, type)
name = obj.__name__ if isclass else obj.__class__.__name__
backend_registry = cls.store.registry.get(backend, {})
plot_class = backend_registry.get(obj if isclass else type(obj), None)
# Special case to handle PlotSelectors
if hasattr(plot_class, 'plot_classes'):
plot_class = next(iter(plot_class.plot_classes.values()))
if visualization is False or plot_class is None:
if pattern is not None:
obj = ParamFilter(obj, ParamFilter.regexp_filter(pattern))
if len(list(obj.param)) <= 1:
return (f'No {name!r} parameters found matching specified pattern {pattern!r}')
info = param.ipython.ParamPager()(obj)
if ansi is False:
info = ansi_escape.sub('', info)
return cls.highlight(pattern, info)
heading = name if isclass else f'{name}: {obj.group} {obj.label}'
prefix = heading
lines = [prefix, cls.object_info(obj, name, backend=backend, ansi=ansi)]
if not isclass:
lines += ['', cls.target_info(obj, ansi=ansi)]
if plot_class is not None:
lines += ['', cls.options_info(plot_class, ansi, pattern=pattern)]
return "\n".join(lines)
@classmethod
def get_target(cls, obj):
objtype=obj.__class__.__name__
group = group_sanitizer(obj.group)
label = ('.' + label_sanitizer(obj.label) if obj.label else '')
target = f'{objtype}.{group}{label}'
return (None, target) if hasattr(obj, 'values') else (target, None)
@classmethod
def target_info(cls, obj, ansi=False):
if isinstance(obj, type): return ''
targets = obj.traverse(cls.get_target)
elements, containers = zip(*targets)
element_set = {el for el in elements if el is not None}
container_set = {c for c in containers if c is not None}
element_info = None
if len(element_set) == 1:
element_info = f'Element: {next(iter(element_set))}'
elif len(element_set) > 1:
element_info = 'Elements:\n %s' % '\n '.join(sorted(element_set))
container_info = None
if len(container_set) == 1:
container_info = f'Container: {next(iter(container_set))}'
elif len(container_set) > 1:
container_info = 'Containers:\n %s' % '\n '.join(sorted(container_set))
heading = cls.heading('Target Specifications', ansi=ansi, char="-")
target_header = '\nTargets in this object available for customization:\n'
if element_info and container_info:
target_info = f'{element_info}\n\n{container_info}'
else:
target_info = element_info if element_info else container_info
target_footer = ("\nTo see the options info for one of these target specifications,"
"\nwhich are of the form {type}[.{group}[.{label}]], do holoviews.help({type}).")
return f'{heading}\n{target_header}\n{target_info}\n{target_footer}'
@classmethod
def object_info(cls, obj, name, backend, ansi=False):
element = not getattr(obj, '_deep_indexable', False)
element_url ='http://holoviews.org/reference/elements/{backend}/{obj}.html'
container_url ='http://holoviews.org/reference/containers/{backend}/{obj}.html'
url = element_url if element else container_url
link = url.format(obj=name, backend=backend)
link = None if element and (name not in cls.elements) else link
msg = ("\nOnline example: {link}" if link else ''
+ "\nHelp for the data object: holoviews.help({obj})"
+ " or holoviews.help(<{lower}_instance>)")
return '\n'.join([msg.format(obj=name, lower=name.lower(), link=link)])
@classmethod
def options_info(cls, plot_class, ansi=False, pattern=None):
if plot_class.style_opts:
backend_name = plot_class.backend
style_info = f"\n(Consult {backend_name}'s documentation for more information.)"
style_keywords = f"\t{', '.join(plot_class.style_opts)}"
style_msg = f'{style_keywords}\n{style_info}'
else:
style_msg = '\t<No style options available>'
param_info = cls.get_parameter_info(plot_class, ansi=ansi, pattern=pattern)
lines = [ cls.heading('Style Options', ansi=ansi, char="-"), '',
style_msg, '',
cls.heading('Plot Options', ansi=ansi, char="-"), '']
if param_info is not None:
lines += ["The plot options are the parameters of the plotting class:\n",
param_info]
elif pattern is not None:
lines+= [f'No {plot_class.__name__!r} parameters found matching specified pattern {pattern!r}.']
else:
lines+= [f'No {plot_class.__name__!r} parameters found.']
return '\n'.join(lines)
class PrettyPrinter(param.Parameterized):
"""
The PrettyPrinter used to print all HoloView objects via the
pprint method.
"""
show_defaults = param.Boolean(default=False, doc="""
Whether to show default options as part of the repr.
If show_options=False this has no effect.""")
show_options = param.Boolean(default=False, doc="""
Whether to show options as part of the repr.""")
tab = ' '
type_formatter= ':{type}'
@bothmethod
def pprint(cls_or_slf, node):
return cls_or_slf.serialize(cls_or_slf.recurse(node))
@bothmethod
def serialize(cls_or_slf, lines):
accumulator = []
for level, line in lines:
accumulator.append((level *cls_or_slf.tab) + line)
return "\n".join(accumulator)
@bothmethod
def shift(cls_or_slf, lines, shift=0):
return [(lvl+shift, line) for (lvl, line) in lines]
@bothmethod
def padding(cls_or_slf, items):
return max(len(p) for p in items) if len(items) > 1 else len(items[0])
@bothmethod
def component_type(cls_or_slf, node):
"Return the type.group.label dotted information"
if node is None: return ''
return cls_or_slf.type_formatter.format(type=str(type(node).__name__))
@bothmethod
def recurse(cls_or_slf, node, attrpath=None, attrpaths=None, siblings=None, level=0, value_dims=True):
"""
Recursive function that builds up an ASCII tree given an
AttrTree node.
"""
if siblings is None:
siblings = []
if attrpaths is None:
attrpaths = []
level, lines = cls_or_slf.node_info(node, attrpath, attrpaths, siblings, level, value_dims)
attrpaths = ['.'.join(k) for k in node.keys()] if hasattr(node, 'children') else []
siblings = [node.get(child) for child in attrpaths]
for attrpath in attrpaths:
lines += cls_or_slf.recurse(node.get(attrpath), attrpath, attrpaths=attrpaths,
siblings=siblings, level=level+1, value_dims=value_dims)
return lines
@bothmethod
def node_info(cls_or_slf, node, attrpath, attrpaths, siblings, level, value_dims):
"""
Given a node, return relevant information.
"""
opts = None
if hasattr(node, 'children'):
(lvl, lines) = (level, [(level, cls_or_slf.component_type(node))])
opts = cls_or_slf.option_info(node)
elif hasattr(node, 'main'):
(lvl, lines) = cls_or_slf.adjointlayout_info(node, siblings, level, value_dims)
elif getattr(node, '_deep_indexable', False):
(lvl, lines) = cls_or_slf.ndmapping_info(node, siblings, level, value_dims)
elif hasattr(node, 'unit_format'):
(lvl, lines) = level, [(level, repr(node))]
else:
(lvl, lines) = cls_or_slf.element_info(node, siblings, level, value_dims)
opts = cls_or_slf.option_info(node)
# The attribute indexing path acts as a prefix (if applicable)
if attrpath is not None:
padding = cls_or_slf.padding(attrpaths)
(fst_lvl, fst_line) = lines[0]
line = '.'+attrpath.ljust(padding) +' ' + fst_line
lines[0] = (fst_lvl, line)
else:
fst_lvl = level
if cls_or_slf.show_options and opts and opts.kwargs:
lines += [(fst_lvl, l) for l in cls_or_slf.format_options(opts)]
return (lvl, lines)
@bothmethod
def element_info(cls_or_slf, node, siblings, level, value_dims):
"""
Return the information summary for an Element. This consists
of the dotted name followed by an value dimension names.
"""
info = cls_or_slf.component_type(node)
if len(node.kdims) >= 1:
info += cls_or_slf.tab + f"[{','.join(d.name for d in node.kdims)}]"
if value_dims and len(node.vdims) >= 1:
info += cls_or_slf.tab + f"({','.join(d.name for d in node.vdims)})"
return level, [(level, info)]
@bothmethod
def option_info(cls_or_slf, node):
if not cls_or_slf.show_options:
return None
from .options import Options, Store
options = {}
for g in Options._option_groups:
gopts = Store.lookup_options(Store.current_backend, node, g,
defaults=cls_or_slf.show_defaults)
if gopts:
options.update(gopts.kwargs)
opts = Options(**{k:v for k,v in options.items() if k != 'backend'})
return opts
@bothmethod
def format_options(cls_or_slf, opts, wrap_count=100):
opt_repr = str(opts)
cls_name = type(opts).__name__
indent = ' '*(len(cls_name)+1)
wrapper = textwrap.TextWrapper(width=wrap_count, subsequent_indent=indent)
return [' | '+l for l in wrapper.wrap(opt_repr)]
@bothmethod
def adjointlayout_info(cls_or_slf, node, siblings, level, value_dims):
first_line = cls_or_slf.component_type(node)
lines = [(level, first_line)]
additional_lines = []
for component in list(node.data.values()):
additional_lines += cls_or_slf.recurse(component, level=level)
lines += cls_or_slf.shift(additional_lines, 1)
return level, lines
@bothmethod
def ndmapping_info(cls_or_slf, node, siblings, level, value_dims):
key_dim_info = f"[{','.join(d.name for d in node.kdims)}]"
first_line = cls_or_slf.component_type(node) + cls_or_slf.tab + key_dim_info
lines = [(level, first_line)]
opts = cls_or_slf.option_info(node)
if cls_or_slf.show_options and opts and opts.kwargs:
lines += [(level, l) for l in cls_or_slf.format_options(opts)]
if len(node.data) == 0:
return level, lines
# .last has different semantics for GridSpace
last = list(node.data.values())[-1]
if last is not None and last._deep_indexable and not hasattr(last, 'children'):
level, additional_lines = cls_or_slf.ndmapping_info(last, [], level, value_dims)
else:
additional_lines = cls_or_slf.recurse(last, level=level, value_dims=value_dims)
lines += cls_or_slf.shift(additional_lines, 1)
return level, lines
__all__ = ['PrettyPrinter', 'InfoPrinter']