Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an apply method to apply functions to elements #3474

Merged
merged 16 commits into from
Mar 7, 2019
Merged
81 changes: 81 additions & 0 deletions holoviews/core/dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@ def __setstate__(self, d):
self.param.warning("Could not unpickle custom style information.")
d['_id'] = opts_id
self.__dict__.update(d)
super(LabelledData, self).__setstate__({})


class Dimensioned(LabelledData):
Expand Down Expand Up @@ -1350,6 +1351,74 @@ def __call__(self, options=None, **kwargs):
return self.opts(options, **kwargs)


def apply(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 .spaces import DynamicMap
from ..util import Dynamic

applies = isinstance(self, (ViewableElement, DynamicMap))
params = {p: val for p, val in kwargs.items()
if isinstance(val, param.Parameter)
and isinstance(val.owner, param.Parameterized)}

if dynamic is None:
dynamic = (bool(streams) or isinstance(self, DynamicMap) or
util.is_param_method(function, has_deps=True) or
params)

if applies and dynamic:
return Dynamic(self, operation=function, streams=streams,
kwargs=kwargs, link_inputs=link_inputs)
elif applies:
inner_kwargs = dict(kwargs)
for k, v in kwargs.items():
if util.is_param_method(v, has_deps=True):
inner_kwargs[k] = v()
elif k in params:
inner_kwargs[k] = getattr(v.owner, v.name)
if hasattr(function, 'dynamic'):
inner_kwargs['dynamic'] = False
return function(self, **inner_kwargs)
elif self._deep_indexable:
mapped = OrderedDict()
for k, v in self.data.items():
new_val = v.apply(function, streams, link_inputs, **kwargs)
if new_val is not None:
mapped[k] = new_val
return self.clone(mapped, link=link_inputs)


def options(self, *args, **kwargs):
"""Applies simplified option definition returning a new object.

Expand Down Expand Up @@ -1440,6 +1509,7 @@ def _repr_mimebundle_(self, include=None, exclude=None):
return Store.render(self)



class ViewableElement(Dimensioned):
"""
A ViewableElement is a dimensioned datastructure that may be
Expand Down Expand Up @@ -1501,6 +1571,17 @@ def _process_items(cls, vals):
return items


def __setstate__(self, d):
"""
Ensure that object does not try to reference its parent during
unpickling.
"""
parent = d.pop('parent', None)
d['parent'] = None
super(AttrTree, self).__setstate__(d)
self.__dict__['parent'] = parent


@classmethod
def _deduplicate_items(cls, items):
"Deduplicates assigned paths by incrementing numbering"
Expand Down
46 changes: 13 additions & 33 deletions holoviews/core/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
"""
import param
from .dimension import ViewableElement
from .element import Element, HoloMap, GridSpace, NdLayout
from .element import Element
from .layout import Layout
from .overlay import NdOverlay, Overlay
from .spaces import DynamicMap, Callable
from .spaces import Callable
from . import util


class Operation(param.ParameterizedFunction):
Expand Down Expand Up @@ -143,38 +144,17 @@ def process_element(self, element, key, **params):
return self._apply(element, key)


def __call__(self, element, **params):
def __call__(self, element, **kwargs):
params = dict(kwargs)
for k, v in kwargs.items():
if util.is_param_method(v, has_deps=True):
params[k] = v()
elif isinstance(v, param.Parameter) and isinstance(v.owner, param.Parameterized):
params[k] = getattr(v.owner, v.name)
self.p = param.ParamOverrides(self, params)
dynamic = ((self.p.dynamic == 'default' and
isinstance(element, DynamicMap))
or self.p.dynamic is True)

if isinstance(element, (GridSpace, NdLayout)):
# Initialize an empty axis layout
grid_data = ((pos, self(cell, **params))
for pos, cell in element.items())
processed = element.clone(grid_data)
elif dynamic:
from ..util import Dynamic
processed = Dynamic(element, streams=self.p.streams,
link_inputs=self.p.link_inputs,
operation=self, kwargs=params)
elif isinstance(element, ViewableElement):
processed = self._apply(element)
elif isinstance(element, DynamicMap):
if any((not d.values) for d in element.kdims):
raise ValueError('Applying a non-dynamic operation requires '
'all DynamicMap key dimensions to define '
'the sampling by specifying values.')
samples = tuple(d.values for d in element.kdims)
processed = self(element[samples], **params)
elif isinstance(element, HoloMap):
mapped_items = [(k, self._apply(el, key=k))
for k, el in element.items()]
processed = element.clone(mapped_items)
else:
raise ValueError("Cannot process type %r" % type(element).__name__)
return processed
if isinstance(element, ViewableElement) and not self.p.dynamic:
return self._apply(element)
return element.apply(self, link_inputs=self.p.link_inputs, **kwargs)



Expand Down
47 changes: 45 additions & 2 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,8 +904,7 @@ def __init__(self, callback, initial_items=None, streams=None, **params):
streams = (streams or [])

# If callback is a parameterized method and watch is disabled add as stream
param_watch_support = util.param_version >= '1.8.0'
if util.is_param_method(callback) and params.get('watch', param_watch_support):
if util.is_param_method(callback, has_deps=True) and params.get('watch', True):
streams.append(callback)

if isinstance(callback, types.GeneratorType):
Expand Down Expand Up @@ -1388,6 +1387,50 @@ def _cache(self, key, val):
self.data.pop(first_key)
self[key] = val

def apply(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.
"""
if dynamic == False:
samples = tuple(d.values for d in self.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[samples]).apply(
function, streams, link_inputs, dynamic, **kwargs)
return super(DynamicMap, self).apply(function, streams, link_inputs, dynamic, **kwargs)


def map(self, map_fn, specs=None, clone=True, link_inputs=True):
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
"""Map a function to all objects matching the specs
Expand Down
1 change: 1 addition & 0 deletions holoviews/core/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def merge(cls, trees):
first.update(tree)
return first


def __dir__(self):
"""
The _dir_mode may be set to 'default' or 'user' in which case
Expand Down
35 changes: 29 additions & 6 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1423,11 +1423,25 @@ def get_param_values(data):
return params


def is_param_method(obj):
"""
Whether the object is a method on a parameterized object.
def is_param_method(obj, has_deps=False):
"""Whether the object is a method on a parameterized object.

Args:
obj: Object to check
has_deps (boolean, optional): Check for dependencies
Whether to also check whether the method has been annotated
with param.depends

Returns:
A boolean value indicating whether the object is a method
on a Parameterized object and if enabled whether it has any
dependencies
"""
return inspect.ismethod(obj) and isinstance(get_method_owner(obj), param.Parameterized)
parameterized = (inspect.ismethod(obj) and
isinstance(get_method_owner(obj), param.Parameterized))
if parameterized and has_deps:
return getattr(obj, "_dinfo", {}).get('dependencies')
return parameterized


@contextmanager
Expand Down Expand Up @@ -1519,12 +1533,21 @@ def stream_parameters(streams, no_duplicates=True, exclude=['name']):
If no_duplicates is enabled, a KeyError will be raised if there are
parameter name clashes across the streams.
"""
param_groups = [s.contents.keys() for s in streams]
param_groups = []
for s in streams:
if not s.contents and isinstance(s.hashkey, dict):
param_groups.append(list(s.hashkey))
else:
param_groups.append(list(s.contents))
names = [name for group in param_groups for name in group]

if no_duplicates:
clashes = sorted(set([n for n in names if names.count(n) > 1]))
clash_streams = [s for s in streams for c in clashes if c in s.contents]
clash_streams = []
for s in streams:
for c in clashes:
if c in s.contents or (not s.contents and isinstance(s.hashkey, dict) and c in s.hashkey):
clash_streams.append(s)
if clashes:
clashing = ', '.join([repr(c) for c in clash_streams[:-1]])
raise Exception('The supplied stream objects %s and %s '
Expand Down
27 changes: 25 additions & 2 deletions holoviews/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from numbers import Number
from collections import defaultdict
from contextlib import contextmanager
from itertools import groupby

import param
import numpy as np
Expand Down Expand Up @@ -293,7 +294,7 @@ def _validate_rename(self, mapping):
for k, v in mapping.items():
if k not in param_names:
raise KeyError('Cannot rename %r as it is not a stream parameter' % k)
if v in param_names:
if k != v and v in param_names:
raise KeyError('Cannot rename to %r as it clashes with a '
'stream parameter of the same name' % v)
return mapping
Expand Down Expand Up @@ -621,11 +622,33 @@ def __init__(self, parameterized, parameters=None, watch=True, **params):
if watch:
self.parameterized.param.watch(self._watcher, self.parameters)

@classmethod
def from_params(cls, params):
"""Returns Params streams given a dictionary of parameters

Args:
params (dict): Dictionary of parameters

Returns:
List of Params streams
"""
key_fn = lambda x: id(x[1].owner)
streams = []
for _, group in groupby(sorted(params.items(), key=key_fn), key_fn):
group = list(group)
inst = [p.owner for _, p in group][0]
if not isinstance(inst, param.Parameterized):
continue
names = [p.name for _, p in group]
rename = {p.name: n for n, p in group}
streams.append(cls(inst, names, rename=rename))
return streams

def _validate_rename(self, mapping):
for k, v in mapping.items():
if k not in self.parameters:
raise KeyError('Cannot rename %r as it is not a stream parameter' % k)
if v in self.parameters:
if k != v and v in self.parameters:
raise KeyError('Cannot rename to %r as it clashes with a '
'stream parameter of the same name' % v)
return mapping
Expand Down
Loading