/
options.py
1949 lines (1596 loc) · 74 KB
/
options.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
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Options and OptionTrees allow different classes of options
(e.g. matplotlib-specific styles and plot specific parameters) to be
defined separately from the core data structures and away from
visualization specific code.
There are three classes that form the options system:
Cycle:
Used to define infinite cycles over a finite set of elements, using
either an explicit list or some pre-defined collection (e.g from
matplotlib rcParams). For instance, a Cycle object can be used loop
a set of display colors for multiple curves on a single axis.
Options:
Containers of arbitrary keyword values, including optional keyword
validation, support for Cycle objects and inheritance.
OptionTree:
A subclass of AttrTree that is used to define the inheritance
relationships between a collection of Options objects. Each node
of the tree supports a group of Options objects and the leaf nodes
inherit their keyword values from parent nodes up to the root.
Store:
A singleton class that stores all global and custom options and
links HoloViews objects, the chosen plotting backend and the IPython
extension together.
"""
import pickle
import traceback
import difflib
import inspect
from contextlib import contextmanager
from collections import defaultdict
import numpy as np
import param
from .tree import AttrTree
from .util import sanitize_identifier, group_sanitizer,label_sanitizer, basestring, OrderedDict
from .util import deprecated_opts_signature, disable_constant, config
from .pprint import InfoPrinter, PrettyPrinter
def cleanup_custom_options(id, weakref=None):
"""
Cleans up unused custom trees if all objects referencing the
custom id have been garbage collected or tree is otherwise
unreferenced.
"""
try:
if Store._options_context:
return
weakrefs = Store._weakrefs.get(id, [])
if weakref in weakrefs:
weakrefs.remove(weakref)
refs = []
for wr in list(weakrefs):
r = wr()
if r is None or r.id != id:
weakrefs.remove(wr)
else:
refs.append(r)
if not refs:
for bk in Store.loaded_backends():
if id in Store._custom_options[bk]:
Store._custom_options[bk].pop(id)
if not weakrefs:
Store._weakrefs.pop(id, None)
except Exception as e:
raise Exception('Cleanup of custom options tree with id %s '
'failed with the following exception: %s, '
'an unreferenced orphan tree may persist in '
'memory' % (e, id))
class SkipRendering(Exception):
"""
A SkipRendering exception in the plotting code will make the display
hooks fall back to a text repr. Used to skip rendering of
DynamicMaps with exhausted element generators.
"""
def __init__(self, message="", warn=True):
self.warn = warn
super(SkipRendering, self).__init__(message)
class Opts(object):
def __init__(self, obj, mode=None):
self._mode = mode
self._obj = obj
def get(self, group=None, backend=None):
"""Returns the corresponding Options object.
Args:
group: The options group. Flattens across groups if None.
backend: Current backend if None otherwise chosen backend.
Returns:
Options object associated with the object containing the
applied option keywords.
"""
keywords = {}
groups = Options._option_groups if group is None else [group]
backend = backend if backend else Store.current_backend
for group in groups:
optsobj = Store.lookup_options(backend, self._obj, group)
keywords = dict(keywords, **optsobj.kwargs)
return Options(**keywords)
def __call__(self, *args, **kwargs):
"""Applies nested options definition.
Applies options on an object or nested group of objects in a
flat format. Unlike the .options method, .opts modifies the
options in place by default. If the options are to be set
directly on the object a simple format may be used, e.g.:
obj.opts(cmap='viridis', show_title=False)
If the object is nested the options must be qualified using
a type[.group][.label] specification, e.g.:
obj.opts('Image', cmap='viridis', show_title=False)
or using:
obj.opts({'Image': dict(cmap='viridis', show_title=False)})
Args:
*args: Sets of options to apply to object
Supports a number of formats including lists of Options
objects, a type[.group][.label] followed by a set of
keyword options to apply and a dictionary indexed by
type[.group][.label] specs.
backend (optional): Backend to apply options to
Defaults to current selected backend
clone (bool, optional): Whether to clone object
Options can be applied in place with clone=False
**kwargs: Keywords of options
Set of options to apply to the object
For backwards compatibility, this method also supports the
option group semantics now offered by the hv.opts.apply_groups
utility. This usage will be deprecated and for more
information see the apply_options_type docstring.
Returns:
Returns the object or a clone with the options applied
"""
if self._mode is None:
return self._base_opts(*args, **kwargs)
elif self._mode == 'holomap':
return self._holomap_opts(*args, **kwargs)
elif self._mode == 'dynamicmap':
return self._dynamicmap_opts(*args, **kwargs)
def clear(self, clone=False):
"""Clears any options applied to the object.
Args:
clone: Whether to return a cleared clone or clear inplace
Returns:
The object cleared of any options applied to it
"""
return self._obj.opts(clone=clone)
def info(self, show_defaults=False):
"""Prints a repr of the object including any applied options.
Args:
show_defaults: Whether to include default options
"""
pprinter = PrettyPrinter(show_options=True, show_defaults=show_defaults)
print(pprinter.pprint(self._obj))
def _holomap_opts(self, *args, **kwargs):
clone = kwargs.pop('clone', None)
apply_groups, _, _ = deprecated_opts_signature(args, kwargs)
data = OrderedDict([(k, v.opts(*args, **kwargs))
for k, v in self._obj.data.items()])
# By default do not clone in .opts method
if (apply_groups if clone is None else clone):
return self._obj.clone(data)
else:
self._obj.data = data
return self._obj
def _dynamicmap_opts(self, *args, **kwargs):
from ..util import Dynamic
clone = kwargs.get('clone', None)
apply_groups, _, _ = deprecated_opts_signature(args, kwargs)
# By default do not clone in .opts method
clone = (apply_groups if clone is None else clone)
obj = self._obj if clone else self._obj.clone()
dmap = Dynamic(obj, operation=lambda obj, **dynkwargs: obj.opts(*args, **kwargs),
streams=self._obj.streams, link_inputs=True)
if not clone:
with disable_constant(self._obj):
obj.callback = self._obj.callback
self._obj.callback = dmap.callback
dmap = self._obj
dmap.data = OrderedDict([(k, v.opts(*args, **kwargs))
for k, v in self._obj.data.items()])
return dmap
def _base_opts(self, *args, **kwargs):
apply_groups, options, new_kwargs = deprecated_opts_signature(args, kwargs)
# By default do not clone in .opts method
clone = kwargs.get('clone', None)
if apply_groups and config.future_deprecations:
msg = ("Calling the .opts method with options broken down by options "
"group (i.e. separate plot, style and norm groups) is deprecated. "
"Use the .options method converting to the simplified format "
"instead or use hv.opts.apply_groups for backward compatibility.")
param.main.warning(msg)
if apply_groups:
from ..util import opts
if options is not None:
kwargs['options'] = options
return opts.apply_groups(self._obj, **dict(kwargs, **new_kwargs))
kwargs['clone'] = False if clone is None else clone
return self._obj.options(*args, **kwargs)
class OptionError(Exception):
"""
Custom exception raised when there is an attempt to apply invalid
options. Stores the necessary information to construct a more
readable message for the user if caught and processed
appropriately.
"""
def __init__(self, invalid_keyword, allowed_keywords,
group_name=None, path=None):
super(OptionError, self).__init__(self.message(invalid_keyword,
allowed_keywords,
group_name, path))
self.invalid_keyword = invalid_keyword
self.allowed_keywords = allowed_keywords
self.group_name =group_name
self.path = path
def message(self, invalid_keyword, allowed_keywords, group_name, path):
msg = ("Invalid option %s, valid options are: %s"
% (repr(invalid_keyword), str(allowed_keywords)))
if path and group_name:
msg = ("Invalid key for group %r on path %r;\n"
% (group_name, path)) + msg
return msg
def format_options_error(self):
"""
Return a fuzzy match message based on the OptionError
"""
allowed_keywords = self.allowed_keywords
target = allowed_keywords.target
matches = allowed_keywords.fuzzy_match(self.invalid_keyword)
if not matches:
matches = allowed_keywords.values
similarity = 'Possible'
else:
similarity = 'Similar'
loaded_backends = Store.loaded_backends()
target = 'for {0}'.format(target) if target else ''
if len(loaded_backends) == 1:
loaded=' in loaded backend {0!r}'.format(loaded_backends[0])
else:
backend_list = ', '.join(['%r'% b for b in loaded_backends[:-1]])
loaded=' in loaded backends {0} and {1!r}'.format(backend_list,
loaded_backends[-1])
suggestion = ("If you believe this keyword is correct, please make sure "
"the backend has been imported or loaded with the "
"hv.extension.")
group = '{0} option'.format(self.group_name) if self.group_name else 'keyword'
msg=('Unexpected {group} {kw} {target}{loaded}.\n\n'
'{similarity} keywords in the currently active '
'{current_backend} renderer are: {matches}\n\n{suggestion}')
return msg.format(kw="'%s'" % self.invalid_keyword,
target=target,
group=group,
loaded=loaded, similarity=similarity,
current_backend=repr(Store.current_backend),
matches=matches,
suggestion=suggestion)
class AbbreviatedException(Exception):
"""
Raised by the abbreviate_exception context manager when it is
appropriate to present an abbreviated the traceback and exception
message in the notebook.
Particularly useful when processing style options supplied by the
user which may not be valid.
"""
def __init__(self, etype, value, traceback):
self.etype = etype
self.value = value
self.traceback = traceback
self.msg = str(value)
def __str__(self):
abbrev = '%s: %s' % (self.etype.__name__, self.msg)
msg = ('To view the original traceback, catch this exception '
'and call print_traceback() method.')
return '%s\n\n%s' % (abbrev, msg)
def print_traceback(self):
"""
Print the traceback of the exception wrapped by the AbbreviatedException.
"""
traceback.print_exception(self.etype, self.value, self.traceback)
class abbreviated_exception(object):
"""
Context manager used to to abbreviate tracebacks using an
AbbreviatedException when a backend may raise an error due to
incorrect style options.
"""
def __enter__(self):
return self
def __exit__(self, etype, value, traceback):
if isinstance(value, Exception):
raise AbbreviatedException(etype, value, traceback)
@contextmanager
def options_policy(skip_invalid, warn_on_skip):
"""
Context manager to temporarily set the skip_invalid and warn_on_skip
class parameters on Options.
"""
settings = (Options.skip_invalid, Options.warn_on_skip)
(Options.skip_invalid, Options.warn_on_skip) = (skip_invalid, warn_on_skip)
yield
(Options.skip_invalid, Options.warn_on_skip) = settings
class Keywords(param.Parameterized):
"""
A keywords objects represents a set of Python keywords. It is
list-like and ordered but it is also a set without duplicates. When
passed as **kwargs, Python keywords are not ordered but this class
always lists keywords in sorted order.
In addition to containing the list of keywords, Keywords has an
optional target which describes what the keywords are applicable to.
This class is for internal use only and should not be in the user
namespace.
"""
values = param.List(doc="Set of keywords as a sorted list.")
target = param.String(allow_None=True, doc="""
Optional string description of what the keywords apply to.""")
def __init__(self, values=[], target=None):
strings = [isinstance(v, (str,basestring)) for v in values]
if False in strings:
raise ValueError('All keywords must be strings: {0}'.format(values))
super(Keywords, self).__init__(values=sorted(values),
target=target)
def __add__(self, other):
if (self.target and other.target) and (self.target != other.target):
raise Exception('Targets must match to combine Keywords')
target = self.target or other.target
return Keywords(sorted(set(self.values + other.values)), target=target)
def fuzzy_match(self, kw):
"""
Given a string, fuzzy match against the Keyword values,
returning a list of close matches.
"""
return difflib.get_close_matches(kw, self.values)
def __repr__(self):
if self.target:
msg = 'Keywords({values}, target={target})'
info = dict(values=self.values, target=self.target)
else:
msg = 'Keywords({values})'
info = dict(values=self.values)
return msg.format(**info)
def __str__(self): return str(self.values)
def __iter__(self): return iter(self.values)
def __bool__(self): return bool(self.values)
def __nonzero__(self): return bool(self.values)
def __contains__(self, val): return val in self.values
class Cycle(param.Parameterized):
"""
A simple container class that specifies cyclic options. A typical
example would be to cycle the curve colors in an Overlay composed
of an arbitrary number of curves. The values may be supplied as
an explicit list or a key to look up in the default cycles
attribute.
"""
key = param.String(default='default_colors', allow_None=True, doc="""
The key in the default_cycles dictionary used to specify the
color cycle if values is not supplied. """)
values = param.List(default=[], doc="""
The values the cycle will iterate over.""")
default_cycles = {'default_colors': []}
def __init__(self, cycle=None, **params):
if cycle is not None:
if isinstance(cycle, basestring):
params['key'] = cycle
else:
params['values'] = cycle
params['key'] = None
super(Cycle, self).__init__(**params)
self.values = self._get_values()
def __getitem__(self, num):
return self(values=self.values[:num])
def _get_values(self):
if self.values: return self.values
elif self.key:
return self.default_cycles[self.key]
else:
raise ValueError("Supply either a key or explicit values.")
def __call__(self, values=None, **params):
values = values if values else self.values
return self.__class__(**dict(self.get_param_values(), values=values, **params))
def __len__(self):
return len(self.values)
def __repr__(self):
if self.key == self.param.params('key').default:
vrepr = ''
elif self.key:
vrepr = repr(self.key)
else:
vrepr = [str(el) for el in self.values]
return "%s(%s)" % (type(self).__name__, vrepr)
def grayscale(val):
return (val, val, val, 1.0)
class Palette(Cycle):
"""
Palettes allow easy specifying a discrete sampling
of an existing colormap. Palettes may be supplied a key
to look up a function function in the colormap class
attribute. The function should accept a float scalar
in the specified range and return a RGB(A) tuple.
The number of samples may also be specified as a
parameter.
The range and samples may conveniently be overridden
with the __getitem__ method.
"""
key = param.String(default='grayscale', doc="""
Palettes look up the Palette values based on some key.""")
range = param.NumericTuple(default=(0, 1), doc="""
The range from which the Palette values are sampled.""")
samples = param.Integer(default=32, doc="""
The number of samples in the given range to supply to
the sample_fn.""")
sample_fn = param.Callable(default=np.linspace, doc="""
The function to generate the samples, by default linear.""")
reverse = param.Boolean(default=False, doc="""
Whether to reverse the palette.""")
# A list of available colormaps
colormaps = {'grayscale': grayscale}
def __init__(self, key, **params):
super(Cycle, self).__init__(key=key, **params)
self.values = self._get_values()
def __getitem__(self, slc):
"""
Provides a convenient interface to override the
range and samples parameters of the Cycle.
Supplying a slice step or index overrides the
number of samples. Unsupplied slice values will be
inherited.
"""
(start, stop), step = self.range, self.samples
if isinstance(slc, slice):
if slc.start is not None:
start = slc.start
if slc.stop is not None:
stop = slc.stop
if slc.step is not None:
step = slc.step
else:
step = slc
return self(range=(start, stop), samples=step)
def _get_values(self):
cmap = self.colormaps[self.key]
(start, stop), steps = self.range, self.samples
samples = [cmap(n) for n in self.sample_fn(start, stop, steps)]
return samples[::-1] if self.reverse else samples
class Options(param.Parameterized):
"""
An Options object holds a collection of keyword options. In
addition, Options support (optional) keyword validation as well as
infinite indexing over the set of supplied cyclic values.
Options support inheritance of setting values via the __call__
method. By calling an Options object with additional keywords, you
can create a new Options object inheriting the parent options.
"""
allowed_keywords = param.ClassSelector(class_=Keywords, doc="""
Optional list of strings corresponding to the allowed keywords.""")
key = param.String(default=None, allow_None=True, doc="""
Optional specification of the options key name. For instance,
key could be 'plot' or 'style'.""")
merge_keywords = param.Boolean(default=True, doc="""
Whether to merge with the existing keywords if the corresponding
node already exists""")
skip_invalid = param.Boolean(default=True, doc="""
Whether all Options instances should skip invalid keywords or
raise and exception. May only be specified at the class level.""")
warn_on_skip = param.Boolean(default=True, doc="""
Whether all Options instances should generate warnings when
skipping over invalid keywords or not. May only be specified at
the class level.""")
_option_groups = ['style', 'plot', 'norm']
def __init__(self, key=None, allowed_keywords=[], merge_keywords=True,
max_cycles=None, backend=None, **kwargs):
invalid_kws = []
for kwarg in sorted(kwargs.keys()):
if allowed_keywords and kwarg not in allowed_keywords:
if self.skip_invalid:
invalid_kws.append(kwarg)
else:
raise OptionError(kwarg, allowed_keywords)
if key and key[0].islower() and key not in self._option_groups:
raise Exception('Key %s does not start with a capitalized element class name and is not a group in %s'
% (repr(key), ', '.join(repr(el) for el in self._option_groups)))
for invalid_kw in invalid_kws:
error = OptionError(invalid_kw, allowed_keywords, group_name=key)
StoreOptions.record_skipped_option(error)
if invalid_kws and self.warn_on_skip:
self.param.warning("Invalid options %s, valid options are: %s"
% (repr(invalid_kws), str(allowed_keywords)))
self.kwargs = OrderedDict([(k,kwargs[k]) for k in sorted(kwargs.keys()) if k not in invalid_kws])
self._options = []
self._max_cycles = max_cycles
allowed_keywords = (allowed_keywords if isinstance(allowed_keywords, Keywords)
else Keywords(allowed_keywords))
super(Options, self).__init__(allowed_keywords=allowed_keywords,
merge_keywords=merge_keywords, key=key)
self.backend = backend
def keywords_target(self, target):
"""
Helper method to easily set the target on the allowed_keywords Keywords.
"""
self.allowed_keywords.target = target
return self
def filtered(self, allowed):
"""
Return a new Options object that is filtered by the specified
list of keys. Mutating self.kwargs to filter is unsafe due to
the option expansion that occurs on initialization.
"""
kws = {k:v for k,v in self.kwargs.items() if k in allowed}
return self.__class__(key=self.key,
allowed_keywords=self.allowed_keywords,
merge_keywords=self.merge_keywords, **kws)
def __call__(self, allowed_keywords=None, **kwargs):
"""
Create a new Options object that inherits the parent options.
"""
allowed_keywords=self.allowed_keywords if allowed_keywords in [None,[]] else allowed_keywords
inherited_style = dict(allowed_keywords=allowed_keywords, **kwargs)
return self.__class__(key=self.key, **dict(self.kwargs, **inherited_style))
def keys(self):
"The keyword names across the supplied options."
return sorted(list(self.kwargs.keys()))
def max_cycles(self, num):
"""
Truncates all contained Palette objects to a maximum number
of samples and returns a new Options object containing the
truncated or resampled Palettes.
"""
kwargs = {kw: (arg[num] if isinstance(arg, Palette) else arg)
for kw, arg in self.kwargs.items()}
return self(max_cycles=num, **kwargs)
@property
def cyclic(self):
"Returns True if the options cycle, otherwise False"
return any(isinstance(val, Cycle) for val in self.kwargs.values())
def __getitem__(self, index):
"""
Infinite cyclic indexing of options over the integers,
looping over the set of defined Cycle objects.
"""
if len(self.kwargs) == 0:
return {}
cycles = {k:v.values for k,v in self.kwargs.items() if isinstance(v, Cycle)}
options = {}
for key, values in cycles.items():
options[key] = values[index % len(values)]
static = {k:v for k,v in self.kwargs.items() if not isinstance(v, Cycle)}
return dict(static, **options)
@property
def options(self):
"Access of the options keywords when no cycles are defined."
if not self.cyclic:
return self[0]
else:
raise Exception("The options property may only be used"
" with non-cyclic Options.")
def __repr__(self):
kws = ', '.join("%s=%r" % (k,self.kwargs[k]) for k in sorted(self.kwargs.keys()))
if self.key and self.key[0].isupper() and kws:
return "%s(%s, %s)" % (self.__class__.__name__, repr(self.key), kws)
elif self.key and self.key[0].isupper():
return "%s(%s)" % (self.__class__.__name__, repr(self.key))
else:
return "%s(%s)" % (self.__class__.__name__, kws)
def __str__(self):
return repr(self)
class OptionTree(AttrTree):
"""
A subclass of AttrTree that is used to define the inheritance
relationships between a collection of Options objects. Each node
of the tree supports a group of Options objects and the leaf nodes
inherit their keyword values from parent nodes up to the root.
Supports the ability to search the tree for the closest valid path
using the find method, or compute the appropriate Options value
given an object and a mode. For a given node of the tree, the
options method computes a Options object containing the result of
inheritance for a given group up to the root of the tree.
When constructing an OptionTree, you can specify the option groups
as a list (i.e empty initial option groups at the root) or as a
dictionary (e.g groups={'style':Option()}). You can also
initialize the OptionTree with the options argument together with
the **kwargs - see StoreOptions.merge_options for more information
on the options specification syntax.
You can use the string specifier '.' to refer to the root node in
the options specification. This acts as an alternative was of
specifying the options groups of the current node. Note that this
approach method may only be used with the group lists format.
"""
def __init__(self, items=None, identifier=None, parent=None,
groups=None, options=None, **kwargs):
if groups is None:
raise ValueError('Please supply groups list or dictionary')
_groups = {g:Options() for g in groups} if isinstance(groups, list) else groups
self.__dict__['groups'] = _groups
self.__dict__['_instantiated'] = False
AttrTree.__init__(self, items, identifier, parent)
self.__dict__['_instantiated'] = True
options = StoreOptions.merge_options(_groups.keys(), options, **kwargs)
root_groups = options.pop('.', None)
if root_groups and isinstance(groups, list):
self.__dict__['groups'] = {g:Options(**root_groups.get(g,{})) for g in _groups.keys()}
elif root_groups:
raise Exception("Group specification as a dictionary only supported if "
"the root node '.' syntax not used in the options.")
if options:
StoreOptions.apply_customizations(options, self)
def _merge_options(self, identifier, group_name, options):
"""
Computes a merged Options object for the given group
name from the existing Options on the node and the
new Options which are passed in.
"""
if group_name not in self.groups:
raise KeyError("Group %s not defined on SettingTree" % group_name)
if identifier in self.children:
current_node = self[identifier]
group_options = current_node.groups[group_name]
else:
#When creating a node (nothing to merge with) ensure it is empty
group_options = Options(group_name,
allowed_keywords=self.groups[group_name].allowed_keywords)
override_kwargs = dict(options.kwargs)
old_allowed = group_options.allowed_keywords
override_kwargs['allowed_keywords'] = options.allowed_keywords + old_allowed
try:
return (group_options(**override_kwargs)
if options.merge_keywords else Options(group_name, **override_kwargs))
except OptionError as e:
raise OptionError(e.invalid_keyword,
e.allowed_keywords,
group_name=group_name,
path = self.path)
def __getitem__(self, item):
if item in self.groups:
return self.groups[item]
return super(OptionTree, self).__getitem__(item)
def __getattr__(self, identifier):
"""
Allows creating sub OptionTree instances using attribute
access, inheriting the group options.
"""
try:
return super(AttrTree, self).__getattr__(identifier)
except AttributeError: pass
if identifier.startswith('_'): raise AttributeError(str(identifier))
elif self.fixed==True: raise AttributeError(self._fixed_error % identifier)
valid_id = sanitize_identifier(identifier, escape=False)
if valid_id in self.children:
return self.__dict__[valid_id]
# When creating a intermediate child node, leave kwargs empty
self.__setattr__(identifier, {k:Options(k, allowed_keywords=v.allowed_keywords)
for k,v in self.groups.items()})
return self[identifier]
def __setattr__(self, identifier, val):
identifier = sanitize_identifier(identifier, escape=False)
new_groups = {}
if isinstance(val, dict):
group_items = val
elif isinstance(val, Options) and val.key is None:
raise AttributeError("Options object needs to have a group name specified.")
elif isinstance(val, Options) and val.key[0].isupper():
raise AttributeError("OptionTree only accepts Options using keys that are one of %s." %
', '.join(repr(el) for el in Options._option_groups))
elif isinstance(val, Options):
group_items = {val.key: val}
elif isinstance(val, OptionTree):
group_items = val.groups
current_node = self[identifier] if identifier in self.children else self
for group_name in current_node.groups:
options = group_items.get(group_name, False)
if options:
new_groups[group_name] = self._merge_options(identifier, group_name, options)
else:
new_groups[group_name] = current_node.groups[group_name]
if new_groups:
data = self[identifier].items() if identifier in self.children else None
new_node = OptionTree(data, identifier=identifier, parent=self, groups=new_groups)
else:
raise ValueError('OptionTree only accepts a dictionary of Options.')
super(OptionTree, self).__setattr__(identifier, new_node)
if isinstance(val, OptionTree):
for subtree in val:
self[identifier].__setattr__(subtree.identifier, subtree)
def find(self, path, mode='node'):
"""
Find the closest node or path to an the arbitrary path that is
supplied down the tree from the given node. The mode argument
may be either 'node' or 'path' which determines the return
type.
"""
path = path.split('.') if isinstance(path, str) else list(path)
item = self
for child in path:
escaped_child = sanitize_identifier(child, escape=False)
matching_children = [c for c in item.children
if child.endswith(c) or escaped_child.endswith(c)]
matching_children = sorted(matching_children, key=lambda x: -len(x))
if matching_children:
item = item[matching_children[0]]
else:
continue
return item if mode == 'node' else item.path
def closest(self, obj, group, defaults=True):
"""
This method is designed to be called from the root of the
tree. Given any LabelledData object, this method will return
the most appropriate Options object, including inheritance.
In addition, closest supports custom options by checking the
object
"""
components = (obj.__class__.__name__,
group_sanitizer(obj.group),
label_sanitizer(obj.label))
target = '.'.join([c for c in components if c])
return self.find(components).options(group, target=target,
defaults=defaults)
def options(self, group, target=None, defaults=True):
"""
Using inheritance up to the root, get the complete Options
object for the given node and the specified group.
"""
if target is None:
target = self.path
if self.groups.get(group, None) is None:
return None
if self.parent is None and target and (self is not Store.options()) and defaults:
root_name = self.__class__.__name__
replacement = root_name + ('' if len(target) == len(root_name) else '.')
option_key = target.replace(replacement,'')
match = Store.options().find(option_key)
if match is not Store.options():
return match.options(group)
else:
return Options()
elif self.parent is None:
return self.groups[group]
parent_opts = self.parent.options(group,target, defaults)
return Options(**dict(parent_opts.kwargs, **self.groups[group].kwargs))
def __repr__(self):
"""
Evalable representation of the OptionTree.
"""
groups = self.__dict__['groups']
# Tab and group entry separators
tab, gsep = ' ', ',\n\n'
# Entry separator and group specifications
esep, gspecs = (",\n"+(tab*2)), []
for group in groups.keys():
especs, accumulator = [], []
if groups[group].kwargs != {}:
accumulator.append(('.', groups[group].kwargs))
for t, v in sorted(self.items()):
kwargs = v.groups[group].kwargs
accumulator.append(('.'.join(t), kwargs))
for (t, kws) in accumulator:
if group=='norm' and all(kws.get(k, False) is False for k in ['axiswise','framewise']):
continue
elif kws:
especs.append((t, kws))
if especs:
format_kws = [(t,'dict(%s)'
% ', '.join('%s=%r' % (k,v) for k,v in sorted(kws.items())))
for t,kws in especs]
ljust = max(len(t) for t,_ in format_kws)
sep = (tab*2) if len(format_kws) >1 else ''
entries = sep + esep.join([sep+'%r : %s' % (t.ljust(ljust),v) for t,v in format_kws])
gspecs.append(('%s%s={\n%s}' if len(format_kws)>1 else '%s%s={%s}') % (tab,group, entries))
return 'OptionTree(groups=%s,\n%s\n)' % (groups.keys(), gsep.join(gspecs))
class Compositor(param.Parameterized):
"""
A Compositor is a way of specifying an operation to be automatically
applied to Overlays that match a specified pattern upon display.
Any Operation that takes an Overlay as input may be used to define a
compositor.
For instance, a compositor may be defined to automatically display
three overlaid monochrome matrices as an RGB image as long as the
values names of those matrices match 'R', 'G' and 'B'.
"""
mode = param.ObjectSelector(default='data',
objects=['data', 'display'], doc="""
The mode of the Compositor object which may be either 'data' or
'display'.""")
backends = param.List(default=[], doc="""
Defines which backends to apply the Compositor for.""")
operation = param.Parameter(doc="""
The Operation to apply when collapsing overlays.""")
pattern = param.String(doc="""
The overlay pattern to be processed. An overlay pattern is a
sequence of elements specified by dotted paths separated by * .
For instance the following pattern specifies three overlayed
matrices with values of 'RedChannel', 'GreenChannel' and
'BlueChannel' respectively:
'Image.RedChannel * Image.GreenChannel * Image.BlueChannel.
This pattern specification could then be associated with the RGB
operation that returns a single RGB matrix for display.""")
group = param.String(allow_None=True, doc="""
The group identifier for the output of this particular compositor""")
kwargs = param.Dict(doc="""
Optional set of parameters to pass to the operation.""")
transfer_options = param.Boolean(default=False, doc="""
Whether to transfer the options from the input to the output.""")
transfer_parameters = param.Boolean(default=False, doc="""