Skip to content

Commit

Permalink
Ensure Dynamic utility subscribes to dependent function (#3980)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Sep 20, 2019
1 parent 3f411fa commit e7bfad9
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 34 deletions.
2 changes: 1 addition & 1 deletion doc/nbpublisher
17 changes: 8 additions & 9 deletions holoviews/core/accessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import absolute_import, unicode_literals

from collections import OrderedDict
from types import FunctionType

import param

Expand Down Expand Up @@ -84,24 +85,22 @@ def function(object, **kwargs):
params = {p: val for p, val in kwargs.items()
if isinstance(val, param.Parameter)
and isinstance(val.owner, param.Parameterized)}
param_methods = {p: val for p, val in kwargs.items()
if util.is_param_method(val, has_deps=True)}

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 param_methods)
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 = 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)
inner_kwargs = util.resolve_dependent_kwargs(kwargs)
if hasattr(function, 'dynamic'):
inner_kwargs['dynamic'] = False
return function(self._obj, **inner_kwargs)
Expand Down
39 changes: 35 additions & 4 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import string, fnmatch
import unicodedata
import datetime as dt

from collections import defaultdict
from functools import partial
from contextlib import contextmanager
from distutils.version import LooseVersion as _LooseVersion

from functools import partial
from threading import Thread, Event
from types import FunctionType

import numpy as np
import param

Expand All @@ -26,7 +28,7 @@
# Python3 compatibility
if sys.version_info.major >= 3:
import builtins as builtins # noqa (compatibility)

basestring = str
unicode = str
long = int
Expand All @@ -38,7 +40,7 @@
LooseVersion = _LooseVersion
else:
import __builtin__ as builtins # noqa (compatibility)

basestring = basestring
unicode = unicode
from itertools import izip
Expand Down Expand Up @@ -1482,6 +1484,35 @@ def is_param_method(obj, has_deps=False):
return parameterized


def resolve_dependent_kwargs(kwargs):
"""Resolves parameter dependencies in the supplied dictionary
Resolves parameter values, Parameterized instance methods and
parameterized functions with dependencies in the supplied
dictionary.
Args:
kwargs (dict): A dictionary of keyword arguments
Returns:
A new dictionary with where any parameter dependencies have been
resolved.
"""
resolved = {}
for k, v in kwargs.items():
if is_param_method(v, has_deps=True):
v = v()
elif isinstance(v, param.Parameter) and isinstance(v.owner, param.Parameterized):
v = getattr(v.owner, v.name)
elif isinstance(v, FunctionType) and hasattr(v, '_dinfo'):
deps = v._dinfo
args = (getattr(p.owner, p.name) for p in deps.get('dependencies', []))
kwargs = {k: getattr(p.owner, p.name) for k, p in deps.get('kw', {}).items()}
v = v(*args, **kwargs)
resolved[k] = v
return resolved


@contextmanager
def disable_constant(parameterized):
"""
Expand Down
9 changes: 5 additions & 4 deletions holoviews/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ class Params(Stream):
parameters = param.List([], constant=True, doc="""
Parameters on the parameterized to watch.""")

def __init__(self, parameterized=None, parameters=None, watch=True, **params):
def __init__(self, parameterized=None, parameters=None, watch=True, watch_only=False, **params):
if util.param_version < '1.8.0' and watch:
raise RuntimeError('Params stream requires param version >= 1.8.0, '
'to support watching parameters.')
Expand All @@ -657,6 +657,7 @@ def __init__(self, parameterized=None, parameters=None, watch=True, **params):
rename.update({(o, k): v for o in owners})
params['rename'] = rename

self._watch_only = watch_only
super(Params, self).__init__(parameterized=parameterized, parameters=parameters, **params)
self._memoize_counter = 0
self._events = []
Expand Down Expand Up @@ -730,6 +731,8 @@ def update(self, **kwargs):

@property
def contents(self):
if self._watch_only:
return {}
filtered = {(p.owner, p.name): getattr(p.owner, p.name) for p in self.parameters}
return {self._rename.get((o, n), n): v for (o, n), v in filtered.items()
if self._rename.get((o, n), True) is not None}
Expand All @@ -752,11 +755,9 @@ def __init__(self, parameterized, parameters=None, watch=True, **params):
parameterized = util.get_method_owner(parameterized)
if not parameters:
parameters = [p.pobj for p in parameterized.param.params_depended_on(method.__name__)]
params['watch_only'] = True
super(ParamMethod, self).__init__(parameterized, parameters, watch, **params)

@property
def contents(self):
return {}



Expand Down
33 changes: 33 additions & 0 deletions holoviews/tests/core/testapply.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,39 @@ def test_element_apply_param_method_with_dependencies(self):
pinst.label = 'Another label'
self.assertEqual(applied[()], self.element.relabel('Another label'))

def test_element_apply_function_with_dependencies(self):
pinst = ParamClass()

@param.depends(pinst.param.label)
def get_label(label):
return label + '!'

applied = self.element.apply('relabel', label=get_label)

# Check stream
self.assertEqual(len(applied.streams), 1)
stream = applied.streams[0]
self.assertIsInstance(stream, Params)
self.assertEqual(stream.parameters, [pinst.param.label])

# Check results
self.assertEqual(applied[()], self.element.relabel('Test!'))

# Ensure subscriber gets called
stream.add_subscriber(lambda **kwargs: applied[()])
pinst.label = 'Another label'
self.assertEqual(applied.last, self.element.relabel('Another label!'))

def test_element_apply_function_with_dependencies_non_dynamic(self):
pinst = ParamClass()

@param.depends(pinst.param.label)
def get_label(label):
return label + '!'

applied = self.element.apply('relabel', dynamic=False, label=get_label)
self.assertEqual(applied, self.element.relabel('Test!'))

def test_element_apply_dynamic_with_param_method(self):
pinst = ParamClass()
applied = self.element.apply(lambda x, label: x.relabel(label), label=pinst.dynamic_label)
Expand Down
24 changes: 8 additions & 16 deletions holoviews/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,12 @@ def _get_streams(self, map_obj, watch=True):
for value in self.p.kwargs.values():
if util.is_param_method(value, has_deps=True):
streams.append(value)
elif isinstance(value, FunctionType) and hasattr(value, '_dinfo'):
dependencies = list(value._dinfo.get('dependencies', []))
dependencies += list(value._dinfo.get('kwargs', {}).values())
params = [d for d in dependencies if isinstance(d, param.Parameter)
and isinstance(d.owner, param.Parameterized)]
streams.append(Params(parameters=params, watch_only=True))

valid, invalid = Stream._process_streams(streams)
if invalid:
Expand All @@ -911,33 +917,19 @@ def _process(self, element, key=None, kwargs={}):
else:
return self.p.operation(element, **kwargs)

def _eval_kwargs(self):
"""Evaluates any parameterized methods in the kwargs"""
evaled_kwargs = {}
for k, v in self.p.kwargs.items():
if util.is_param_method(v):
v = v()
elif isinstance(v, FunctionType) and hasattr(v, '_dinfo'):
deps = v._dinfo
args = (getattr(p.owner, p.name) for p in deps.get('dependencies', []))
kwargs = {k: getattr(p.owner, p.name) for k, p in deps.get('kw', {}).items()}
v = v(*args, **kwargs)
evaled_kwargs[k] = v
return evaled_kwargs

def _dynamic_operation(self, map_obj):
"""
Generate function to dynamically apply the operation.
Wraps an existing HoloMap or DynamicMap.
"""
if not isinstance(map_obj, DynamicMap):
def dynamic_operation(*key, **kwargs):
kwargs = dict(self._eval_kwargs(), **kwargs)
kwargs = dict(util.resolve_dependent_kwargs(self.p.kwargs), **kwargs)
obj = map_obj[key] if isinstance(map_obj, HoloMap) else map_obj
return self._process(obj, key, kwargs)
else:
def dynamic_operation(*key, **kwargs):
kwargs = dict(self._eval_kwargs(), **kwargs)
kwargs = dict(util.resolve_dependent_kwargs(self.p.kwargs), **kwargs)
if map_obj._posarg_keys and not key:
key = tuple(kwargs[k] for k in map_obj._posarg_keys)
return self._process(map_obj[key], key, kwargs)
Expand Down

0 comments on commit e7bfad9

Please sign in to comment.