-
-
Notifications
You must be signed in to change notification settings - Fork 395
/
settings.py
426 lines (361 loc) · 17.2 KB
/
settings.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
416
417
418
419
420
421
422
423
424
425
426
from collections import defaultdict, OrderedDict
from ..core import Store
class KeywordSettings:
"""
Base class for options settings used to specified collections of
keyword options.
"""
# Dictionary from keywords to allowed bounds/values
allowed = {}
defaults = OrderedDict([]) # Default keyword values.
options = OrderedDict(defaults.items()) # Current options
# Callables accepting (value, keyword, allowed) for custom exceptions
custom_exceptions = {}
# Hidden. Options that won't tab complete (for backward compatibility)
hidden = {}
@classmethod
def update_options(cls, options, items):
"""
Allows updating options depending on class attributes
and unvalidated options.
"""
@classmethod
def get_options(cls, items, options, warnfn):
"Given a keyword specification, validate and compute options"
options = cls.update_options(options, items)
for keyword in cls.defaults:
if keyword in items:
value = items[keyword]
allowed = cls.allowed[keyword]
if isinstance(allowed, set): pass
elif isinstance(allowed, dict):
if not isinstance(value, dict):
raise ValueError(f"Value {value!r} not a dict type")
disallowed = set(value.keys()) - set(allowed.keys())
if disallowed:
raise ValueError(f"Keywords {disallowed!r} for {keyword!r} option not one of {allowed}")
wrong_type = {k: v for k, v in value.items()
if not isinstance(v, allowed[k])}
if wrong_type:
errors = []
for k,v in wrong_type.items():
errors.append(f"Value {v!r} for {keyword!r} option's {k!r} attribute not of type {allowed[k]!r}")
raise ValueError('\n'.join(errors))
elif isinstance(allowed, list) and value not in allowed:
if keyword in cls.custom_exceptions:
cls.custom_exceptions[keyword](value, keyword, allowed)
else:
raise ValueError(f"Value {value!r} for key {keyword!r} not one of {allowed}")
elif isinstance(allowed, tuple):
if not (allowed[0] <= value <= allowed[1]):
info = (keyword,value)+allowed
raise ValueError("Value {!r} for key {!r} not between {} and {}".format(*info))
options[keyword] = value
return cls._validate(options, items, warnfn)
@classmethod
def _validate(cls, options, items, warnfn):
"Allows subclasses to check options are valid."
raise NotImplementedError("KeywordSettings is an abstract base class.")
@classmethod
def extract_keywords(cls, line, items):
"""
Given the keyword string, parse a dictionary of options.
"""
unprocessed = list(reversed(line.split('=')))
while unprocessed:
chunk = unprocessed.pop()
key = None
if chunk.strip() in cls.allowed:
key = chunk.strip()
else:
raise SyntaxError(f"Invalid keyword: {chunk.strip()}")
# The next chunk may end in a subsequent keyword
value = unprocessed.pop().strip()
if len(unprocessed) != 0:
# Check if a new keyword has begun
for option in cls.allowed:
if value.endswith(option):
value = value[:-len(option)].strip()
unprocessed.append(option)
break
else:
raise SyntaxError(f"Invalid keyword: {value.split()[-1]}")
keyword = f'{key}={value}'
try:
items.update(eval(f'dict({keyword})'))
except Exception:
raise SyntaxError(f"Could not evaluate keyword: {keyword}")
return items
def list_backends():
backends = []
for backend in Store.renderers:
backends.append(backend)
renderer = Store.renderers[backend]
modes = [mode for mode in renderer.param.objects('existing')['mode'].objects
if mode != 'default']
backends += [f'{backend}:{mode}' for mode in modes]
return backends
def list_formats(format_type, backend=None):
"""
Returns list of supported formats for a particular
backend.
"""
if backend is None:
backend = Store.current_backend
mode = Store.renderers[backend].mode if backend in Store.renderers else None
else:
split = backend.split(':')
backend, mode = split if len(split)==2 else (split[0], 'default')
if backend in Store.renderers:
return Store.renderers[backend].mode_formats[format_type]
else:
return []
class OutputSettings(KeywordSettings):
"""
Class for controlling display and output settings.
"""
# Lists: strict options, Set: suggested options, Tuple: numeric bounds.
allowed = {'backend' : list_backends(),
'center' : [True, False],
'fig' : list_formats('fig'),
'holomap' : list_formats('holomap'),
'widgets' : ['embed', 'live'],
'fps' : (0, float('inf')),
'max_frames' : (0, float('inf')),
'max_branches' : {None}, # Deprecated
'size' : (0, float('inf')),
'dpi' : (1, float('inf')),
'filename' : {None},
'info' : [True, False],
'widget_location' : [
'left', 'bottom', 'right', 'top', 'top_left', 'top_right',
'bottom_left', 'bottom_right', 'left_top', 'left_bottom',
'right_top', 'right_bottom'],
'css' : {k: str
for k in ['width', 'height', 'padding', 'margin',
'max-width', 'min-width', 'max-height',
'min-height', 'outline', 'float']}}
defaults = OrderedDict([('backend' , None),
('center' , True),
('fig' , None),
('holomap' , None),
('widgets' , None),
('fps' , None),
('max_frames' , 500),
('size' , None),
('dpi' , None),
('filename' , None),
('info' , False),
('widget_location', None),
('css' , None)])
# Defines the options the OutputSettings remembers. All other options
# are held by the backend specific Renderer.
remembered = ['max_frames', 'info', 'filename']
# Remaining backend specific options renderer options
render_params = ['fig', 'holomap', 'size', 'fps', 'dpi', 'css',
'widget_mode', 'mode', 'widget_location', 'center']
options = OrderedDict()
_backend_options = defaultdict(dict)
# Used to disable info output in testing
_disable_info_output = False
#==========================#
# Backend state management #
#==========================#
last_backend = None
backend_list = [] # List of possible backends
def missing_dependency_exception(value, keyword, allowed):
raise Exception(f"Format {value!r} does not appear to be supported.")
def missing_backend_exception(value, keyword, allowed):
if value in OutputSettings.backend_list:
raise ValueError(f"Backend {value!r} not available. Has it been loaded with the notebook_extension?")
else:
raise ValueError(f"Backend {value!r} does not exist")
custom_exceptions = {'holomap':missing_dependency_exception,
'backend': missing_backend_exception}
# Counter for nbagg figures
nbagg_counter = 0
@classmethod
def _generate_docstring(cls, signature=False):
intro = ["Helper used to set HoloViews display options.",
"Arguments are supplied as a series of keywords in any order:", '']
backend = "backend : The backend used by HoloViews"
fig = "fig : The static figure format"
holomap = "holomap : The display type for holomaps"
widgets = "widgets : The widget mode for widgets"
fps = "fps : The frames per second used for animations"
max_frames= ("max_frames : The max number of frames rendered (default %r)"
% cls.defaults['max_frames'])
size = "size : The percentage size of displayed output"
dpi = "dpi : The rendered dpi of the figure"
filename = ("filename : The filename of the saved output, if any (default %r)"
% cls.defaults['filename'])
info = ("info : The information to page about the displayed objects (default %r)"
% cls.defaults['info'])
css = ("css : Optional css style attributes to apply to the figure image tag")
widget_location = "widget_location : The position of the widgets relative to the plot"
descriptions = [backend, fig, holomap, widgets, fps, max_frames, size,
dpi, filename, info, css, widget_location]
keywords = ['backend', 'fig', 'holomap', 'widgets', 'fps', 'max_frames',
'size', 'dpi', 'filename', 'info', 'css', 'widget_location']
if signature:
doc_signature = '\noutput(%s)\n' % ', '.join('%s=None' % kw for kw in keywords)
return '\n'.join([doc_signature] + intro + descriptions)
else:
return '\n'.join(intro + descriptions)
@classmethod
def _generate_signature(cls):
from inspect import Signature, Parameter
keywords = ['backend', 'fig', 'holomap', 'widgets', 'fps', 'max_frames',
'size', 'dpi', 'filename', 'info', 'css', 'widget_location']
return Signature([Parameter(kw, Parameter.KEYWORD_ONLY) for kw in keywords])
@classmethod
def _validate(cls, options, items, warnfn):
"Validation of edge cases and incompatible options"
if 'html' in Store.display_formats:
pass
elif 'fig' in items and items['fig'] not in Store.display_formats:
msg = (f"Requesting output figure format {items['fig']!r} "
+ f"not in display formats {Store.display_formats!r}")
if warnfn is None:
print(f'Warning: {msg}')
else:
warnfn(msg)
backend = Store.current_backend
return Store.renderers[backend].validate(options)
@classmethod
def output(cls, line=None, cell=None, cell_runner=None,
help_prompt=None, warnfn=None, **kwargs):
if line and kwargs:
raise ValueError('Please either specify a string to '
'parse or keyword arguments')
elif not Store.renderers:
raise ValueError("No plotting extension is currently loaded. "
"Ensure you load an plotting extension with "
"hv.extension or import it explicitly from "
"holoviews.plotting before using hv.output.")
# Make backup of previous options
prev_backend = Store.current_backend
if prev_backend in Store.renderers:
prev_renderer = Store.renderers[prev_backend]
prev_backend_spec = prev_backend+':'+prev_renderer.mode
prev_params = {k: v for k, v in prev_renderer.param.values().items()
if k in cls.render_params}
else:
prev_renderer = None
prev_backend_spec = prev_backend+':default'
prev_params = {}
backend = prev_backend
prev_restore = dict(OutputSettings.options)
try:
if line is not None:
# Parse line
line = line.split('#')[0].strip()
kwargs = cls.extract_keywords(line, OrderedDict())
options = cls.get_options(kwargs, {}, warnfn)
# Make backup of options on selected renderer
if 'backend' in options:
backend_spec = options['backend']
if ':' not in backend_spec:
backend_spec += ':default'
else:
backend_spec = prev_backend_spec
backend = backend_spec.split(':')[0]
renderer = Store.renderers[backend]
render_params = {k: v for k, v in renderer.param.values().items()
if k in cls.render_params}
# Set options on selected renderer and set display hook options
OutputSettings.options = options
cls._set_render_options(options, backend_spec)
except Exception as e:
# If setting options failed ensure they are reset
OutputSettings.options = prev_restore
cls.set_backend(prev_backend)
if backend not in Store.renderers:
raise ValueError("The selected plotting extension {ext} "
"has not been loaded, ensure you load it "
"with hv.extension({ext}) before using "
"hv.output.".format(ext=repr(backend)))
print(f'Error: {e}')
if help_prompt:
print(help_prompt)
return
if cell is not None:
if cell_runner: cell_runner(cell,renderer)
# After cell restore previous options and restore
# temporarily selected renderer
OutputSettings.options = prev_restore
cls._set_render_options(render_params, backend_spec)
if backend_spec.split(':')[0] != prev_backend:
cls.set_backend(prev_backend)
cls._set_render_options(prev_params, prev_backend_spec)
@classmethod
def update_options(cls, options, items):
"""
Switch default options and backend if new backend is supplied in
items.
"""
# Get new backend
backend_spec = items.get('backend', Store.current_backend)
split = backend_spec.split(':')
backend, mode = split if len(split)==2 else (split[0], 'default')
if ':' not in backend_spec:
backend_spec += ':default'
if 'max_branches' in items:
print('Warning: The max_branches option is now deprecated. Ignoring.')
del items['max_branches']
# Get previous backend
prev_backend = Store.current_backend
renderer = Store.renderers[prev_backend]
prev_backend_spec = prev_backend+':'+renderer.mode
# Update allowed formats
for p in ['fig', 'holomap']:
cls.allowed[p] = list_formats(p, backend_spec)
# Return if backend invalid and let validation error
if backend not in Store.renderers:
options['backend'] = backend_spec
return options
# Get backend specific options
backend_options = dict(cls._backend_options[backend_spec])
cls._backend_options[prev_backend_spec] = {k: v for k, v in cls.options.items()
if k in cls.remembered}
# Fill in remembered options with defaults
for opt in cls.remembered:
if opt not in backend_options:
backend_options[opt] = cls.defaults[opt]
# Switch format if mode does not allow it
for p in ['fig', 'holomap']:
if backend_options.get(p) not in cls.allowed[p]:
backend_options[p] = cls.allowed[p][0]
# Ensure backend and mode are set
backend_options['backend'] = backend_spec
backend_options['mode'] = mode
return backend_options
@classmethod
def initialize(cls, backend_list):
cls.backend_list = backend_list
backend = Store.current_backend
if backend in Store.renderers:
cls.options = dict({k: cls.defaults[k] for k in cls.remembered})
cls.set_backend(backend)
else:
cls.options['backend'] = None
cls.set_backend(None)
@classmethod
def set_backend(cls, backend):
cls.last_backend = Store.current_backend
Store.set_current_backend(backend)
@classmethod
def _set_render_options(cls, options, backend=None):
"""
Set options on current Renderer.
"""
if backend:
backend = backend.split(':')[0]
else:
backend = Store.current_backend
cls.set_backend(backend)
if 'widgets' in options:
options['widget_mode'] = options['widgets']
renderer = Store.renderers[backend]
render_options = {k: options[k] for k in cls.render_params if k in options}
renderer.param.update(**render_options)