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

Ensure Dynamic utility subscribes to dependent function #3980

Merged
merged 4 commits into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
38 changes: 34 additions & 4 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
import unicodedata
import datetime as dt

from distutils.version import LooseVersion as _LooseVersion
from functools import partial
from collections import defaultdict
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 +27,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 +39,7 @@
LooseVersion = _LooseVersion
else:
import __builtin__ as builtins # noqa (compatibility)

basestring = basestring
unicode = unicode
from itertools import izip
Expand Down Expand Up @@ -1466,6 +1467,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 @@ -635,7 +635,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 @@ -655,6 +655,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 @@ -728,6 +729,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 @@ -750,11 +753,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 @@ -898,6 +898,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 @@ -915,33 +921,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