-
-
Notifications
You must be signed in to change notification settings - Fork 394
/
accessors.py
512 lines (414 loc) · 19.5 KB
/
accessors.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
"""
Module for accessor objects for viewable HoloViews objects.
"""
from __future__ import absolute_import, unicode_literals
from collections import OrderedDict
from types import FunctionType
import copy
import param
from param.parameterized import add_metaclass
from . import util
from .pprint import PrettyPrinter
class AccessorPipelineMeta(type):
def __new__(mcs, classname, bases, classdict):
if '__call__' in classdict:
classdict['__call__'] = mcs.pipelined(classdict['__call__'])
inst = type.__new__(mcs, classname, bases, classdict)
inst._in_method = False
return inst
@classmethod
def pipelined(mcs, __call__):
def pipelined_call(*args, **kwargs):
from .data import Dataset, MultiDimensionalMapping
inst = args[0]
if not hasattr(inst._obj, '_pipeline'):
# Wrapped object doesn't support the pipeline property
return __call__(*args, **kwargs)
inst_pipeline = copy.copy(inst._obj. _pipeline)
in_method = inst._obj._in_method
if not in_method:
inst._obj._in_method = True
result = __call__(*args, **kwargs)
if not in_method:
mode = getattr(inst, 'mode', None)
if isinstance(result, Dataset):
result._pipeline = inst_pipeline + [
(type(inst), [], {'mode': mode}),
(__call__, list(args[1:]), kwargs)
]
elif isinstance(result, MultiDimensionalMapping):
for key, element in result.items():
element._pipeline = inst_pipeline + [
(type(inst), [], {'mode': mode}),
(__call__, list(args[1:]), kwargs),
(getattr(type(result), '__getitem__'), [key], {})
]
inst._obj._in_method = False
return result
pipelined_call.__doc__ = __call__.__doc__
return pipelined_call
@add_metaclass(AccessorPipelineMeta)
class Apply(object):
"""
Utility to apply a function or operation to all viewable elements
inside the object.
"""
def __init__(self, obj, mode=None):
self._obj = obj
def __call__(self, function, streams=[], link_inputs=True, dynamic=None, **kwargs):
"""Applies a function to all (Nd)Overlay or Element objects.
Any keyword arguments are passed through to the function. If
keyword arguments are instance parameters, or streams are
supplied the returned object will dynamically update in
response to changes in those objects.
Args:
function: A callable function
The function will be passed the return value of the
DynamicMap as the first argument and any supplied
stream values or keywords as additional keyword
arguments.
streams (list, optional): A list of Stream objects
The Stream objects can dynamically supply values which
will be passed to the function as keywords.
link_inputs (bool, optional): Whether to link the inputs
Determines whether Streams and Links attached to
original object will be inherited.
dynamic (bool, optional): Whether to make object dynamic
By default object is made dynamic if streams are
supplied, an instance parameter is supplied as a
keyword argument, or the supplied function is a
parameterized method.
kwargs (dict, optional): Additional keyword arguments
Keyword arguments which will be supplied to the
function.
Returns:
A new object where the function was applied to all
contained (Nd)Overlay or Element objects.
"""
from .dimension import ViewableElement
from .spaces import HoloMap, DynamicMap
from ..util import Dynamic
if isinstance(self._obj, DynamicMap) and dynamic == False:
samples = tuple(d.values for d in self._obj.kdims)
if not all(samples):
raise ValueError('Applying a function to a DynamicMap '
'and setting dynamic=False is only '
'possible if key dimensions define '
'a discrete parameter space.')
return HoloMap(self._obj[samples]).apply(
function, streams, link_inputs, dynamic, **kwargs)
if isinstance(function, util.basestring):
args = kwargs.pop('_method_args', ())
method_name = function
def function(object, **kwargs):
method = getattr(object, method_name, None)
if method is None:
raise AttributeError('Applied method %s does not exist.'
'When declaring a method to apply '
'as a string ensure a corresponding '
'method exists on the object.' %
method_name)
return method(*args, **kwargs)
applies = isinstance(self._obj, (ViewableElement, HoloMap))
params = {p: val for p, val in kwargs.items()
if isinstance(val, param.Parameter)
and isinstance(val.owner, param.Parameterized)}
dependent_kws = any(
(isinstance(val, FunctionType) and hasattr(val, '_dinfo')) or
util.is_param_method(val, has_deps=True) for val in kwargs.values()
)
if dynamic is None:
dynamic = (bool(streams) or isinstance(self._obj, DynamicMap) or
util.is_param_method(function, has_deps=True) or
params or dependent_kws)
if applies and dynamic:
return Dynamic(self._obj, operation=function, streams=streams,
kwargs=kwargs, link_inputs=link_inputs)
elif applies:
inner_kwargs = util.resolve_dependent_kwargs(kwargs)
if hasattr(function, 'dynamic'):
inner_kwargs['dynamic'] = False
return function(self._obj, **inner_kwargs)
elif self._obj._deep_indexable:
mapped = []
for k, v in self._obj.data.items():
new_val = v.apply(function, dynamic=dynamic, streams=streams,
link_inputs=link_inputs, **kwargs)
if new_val is not None:
mapped.append((k, new_val))
return self._obj.clone(mapped, link=link_inputs)
def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs):
"""Applies a aggregate function to all ViewableElements.
See :py:meth:`Dimensioned.opts` and :py:meth:`Apply.__call__`
for more information.
"""
kwargs['_method_args'] = (dimensions, function, spreadfn)
return self.__call__('aggregate', **kwargs)
def opts(self, *args, **kwargs):
"""Applies options to all ViewableElement objects.
See :py:meth:`Dimensioned.opts` and :py:meth:`Apply.__call__`
for more information.
"""
kwargs['_method_args'] = args
return self.__call__('opts', **kwargs)
def reduce(self, dimensions=[], function=None, spreadfn=None, **kwargs):
"""Applies a reduce function to all ViewableElement objects.
See :py:meth:`Dimensioned.opts` and :py:meth:`Apply.__call__`
for more information.
"""
kwargs['_method_args'] = (dimensions, function, spreadfn)
return self.__call__('reduce', **kwargs)
def select(self, **kwargs):
"""Applies a selection to all ViewableElement objects.
See :py:meth:`Dimensioned.opts` and :py:meth:`Apply.__call__`
for more information.
"""
return self.__call__('select', **kwargs)
@add_metaclass(AccessorPipelineMeta)
class Redim(object):
"""
Utility that supports re-dimensioning any HoloViews object via the
redim method.
"""
def __init__(self, obj, mode=None):
self._obj = obj
# Can be 'dataset', 'dynamic' or None
self.mode = mode
def __str__(self):
return "<holoviews.core.dimension.redim method>"
@classmethod
def replace_dimensions(cls, dimensions, overrides):
"""Replaces dimensions in list with dictionary of overrides.
Args:
dimensions: List of dimensions
overrides: Dictionary of dimension specs indexed by name
Returns:
list: List of dimensions with replacements applied
"""
from .dimension import Dimension
replaced = []
for d in dimensions:
if d.name in overrides:
override = overrides[d.name]
elif d.label in overrides:
override = overrides[d.label]
else:
override = None
if override is None:
replaced.append(d)
elif isinstance(override, (util.basestring, tuple)):
replaced.append(d.clone(override))
elif isinstance(override, Dimension):
replaced.append(override)
elif isinstance(override, dict):
replaced.append(d.clone(override.get('name',None),
**{k:v for k,v in override.items() if k != 'name'}))
else:
raise ValueError('Dimension can only be overridden '
'with another dimension or a dictionary '
'of attributes')
return replaced
def _filter_cache(self, dmap, kdims):
"""
Returns a filtered version of the DynamicMap cache leaving only
keys consistently with the newly specified values
"""
filtered = []
for key, value in dmap.data.items():
if not any(kd.values and v not in kd.values for kd, v in zip(kdims, key)):
filtered.append((key, value))
return filtered
def __call__(self, specs=None, **dimensions):
"""
Replace dimensions on the dataset and allows renaming
dimensions in the dataset. Dimension mapping should map
between the old dimension name and a dictionary of the new
attributes, a completely new dimension or a new string name.
"""
obj = self._obj
redimmed = obj
if obj._deep_indexable and self.mode != 'dataset':
deep_mapped = [(k, v.redim(specs, **dimensions))
for k, v in obj.items()]
redimmed = obj.clone(deep_mapped)
if specs is not None:
if not isinstance(specs, list):
specs = [specs]
matches = any(obj.matches(spec) for spec in specs)
if self.mode != 'dynamic' and not matches:
return redimmed
kdims = self.replace_dimensions(obj.kdims, dimensions)
vdims = self.replace_dimensions(obj.vdims, dimensions)
zipped_dims = zip(obj.kdims+obj.vdims, kdims+vdims)
renames = {pk.name: nk for pk, nk in zipped_dims if pk != nk}
if self.mode == 'dataset':
data = obj.data
if renames:
data = obj.interface.redim(obj, renames)
clone = obj.clone(data, kdims=kdims, vdims=vdims)
if self._obj.dimensions(label='name') == clone.dimensions(label='name'):
# Ensure that plot_id is inherited as long as dimension
# name does not change
clone._plot_id = self._obj._plot_id
return clone
if self.mode != 'dynamic':
return redimmed.clone(kdims=kdims, vdims=vdims)
from ..util import Dynamic
def dynamic_redim(obj, **dynkwargs):
return obj.redim(specs, **dimensions)
dmap = Dynamic(obj, streams=obj.streams, operation=dynamic_redim)
dmap.data = OrderedDict(self._filter_cache(redimmed, kdims))
with util.disable_constant(dmap):
dmap.kdims = kdims
dmap.vdims = vdims
return dmap
def _redim(self, name, specs, **dims):
dimensions = {k:{name:v} for k,v in dims.items()}
return self(specs, **dimensions)
def cyclic(self, specs=None, **values):
return self._redim('cyclic', specs, **values)
def value_format(self, specs=None, **values):
return self._redim('value_format', specs, **values)
def range(self, specs=None, **values):
return self._redim('range', specs, **values)
def label(self, specs=None, **values):
for k, v in values.items():
dim = self._obj.get_dimension(k)
if dim and dim.name != dim.label and dim.label != v:
raise ValueError('Cannot override an existing Dimension label')
return self._redim('label', specs, **values)
def soft_range(self, specs=None, **values):
return self._redim('soft_range', specs, **values)
def type(self, specs=None, **values):
return self._redim('type', specs, **values)
def step(self, specs=None, **values):
return self._redim('step', specs, **values)
def default(self, specs=None, **values):
return self._redim('default', specs, **values)
def unit(self, specs=None, **values):
return self._redim('unit', specs, **values)
def values(self, specs=None, **ranges):
return self._redim('values', specs, **ranges)
@add_metaclass(AccessorPipelineMeta)
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.
"""
from .options import Store, Options
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:
apply_groups, _, _ = util.deprecated_opts_signature(args, kwargs)
if apply_groups and util.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)
return self._dispatch_opts( *args, **kwargs)
def _dispatch_opts(self, *args, **kwargs):
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, _, _ = util.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, _, _ = util.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 util.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 = util.deprecated_opts_signature(args, kwargs)
# By default do not clone in .opts method
clone = kwargs.get('clone', None)
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)