Skip to content

Commit

Permalink
Merge pull request #1232 from ioam/preprocessor_refactor
Browse files Browse the repository at this point in the history
Preprocessor refactor
  • Loading branch information
philippjfr authored Mar 29, 2017
2 parents 5160d11 + 65131a2 commit 25fde35
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 85 deletions.
142 changes: 66 additions & 76 deletions holoviews/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,60 +10,6 @@
from .core import util


class Preprocessor(param.Parameterized):
"""
A Preprocessor is a callable that takes a dictionary as an argument
and returns a dictionary. Where possible, Preprocessors should have
valid reprs that can be evaluated.
Preprocessors are used to set the contents of a stream based on the
parameter values. They may be used for debugging purposes or to
remap or repack parameter values before they are passed onto to the
subscribers.
"""

def __call__(self, params):
return params



class Rename(Preprocessor):
"""
A preprocessor used to rename parameter values.
"""

mapping = param.Dict(default={}, doc="""
The mapping from the parameter names to the designated names""")

def __init__(self, **mapping):
super(Rename, self).__init__(mapping=mapping)

def __call__(self, params):
return {self.mapping.get(k,k):v for (k,v) in params.items()}

def __repr__(self):
keywords = ','.join('%s=%r' % (k,v) for (k,v) in sorted(self.mapping.items()))
return 'Rename(%s)' % keywords



class Group(Preprocessor):
"""
A preprocessor that keeps the parameter dictionary together,
supplying it as a value associated with the given key.
"""

def __init__(self, key):
super(Group, self).__init__(key=key)

def __call__(self, params):
return {self.key:params}

def __repr__(self):
return 'Group(%r)' % self.key



class Stream(param.Parameterized):
"""
A Stream is simply a parameterized object with parameters that
Expand Down Expand Up @@ -115,11 +61,11 @@ def trigger(cls, streams):
stream.deactivate()


def __init__(self, preprocessors=[], source=None, subscribers=[],
def __init__(self, rename={}, source=None, subscribers=[],
linked=True, **params):
"""
Mapping allows multiple streams with similar event state to be
used by remapping parameter names.
The rename argument allows multiple streams with similar event
state to be used by remapping parameter names.
Source is an optional argument specifying the HoloViews
datastructure that the stream receives events from, as supported
Expand All @@ -130,9 +76,9 @@ def __init__(self, preprocessors=[], source=None, subscribers=[],
"""
self._source = source
self.subscribers = subscribers
self.preprocessors = preprocessors
self._hidden_subscribers = []
self.linked = linked
self._rename = self._validate_rename(rename)

# The metadata may provide information about the currently
# active event, i.e. the source of the stream values may
Expand All @@ -143,6 +89,29 @@ def __init__(self, preprocessors=[], source=None, subscribers=[],
if source:
self.registry[id(source)].append(self)

def _validate_rename(self, mapping):
param_names = [k for k in self.params().keys() if k != 'name']
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:
raise KeyError('Cannot rename to %r as it clashes with a '
'stream parameter of the same name' % v)
return mapping

def rename(self, **mapping):
"""
The rename method allows stream parameters to be allocated to
new names to avoid clashes with other stream parameters of the
same name. Returns a new clone of the stream instance with the
specified name mapping.
"""
params = {k:v for k,v in self.get_param_values() if k != 'name'}
return self.__class__(rename=mapping,
source=self._source,
subscribers=self.subscribers,
linked=self.linked, **params)


def deactivate(self):
"""
Expand All @@ -164,21 +133,24 @@ def source(self, source):
self.registry[id(source)].append(self)


def transform(self):
"""
Method that can be overwritten by subclasses to process the
parameter values before renaming is applied. Returns a
dictionary of transformed parameters.
"""
return {}

@property
def contents(self):
remapped = {k:v for k,v in self.get_param_values() if k!= 'name' }
for preprocessor in self.preprocessors:
remapped = preprocessor(remapped)
return remapped
filtered = {k:v for k,v in self.get_param_values() if k!= 'name' }
return {self._rename.get(k,k):v for (k,v) in filtered.items()}


def update(self, trigger=True, **kwargs):
def _set_stream_parameters(self, **kwargs):
"""
The update method updates the stream parameters in response to
some event.
If trigger is enabled, the trigger classmethod is invoked on
this particular Stream instance.
Sets the stream parameters which are expected to be declared
constant.
"""
params = self.params().values()
constants = [p.constant for p in params]
Expand All @@ -188,6 +160,19 @@ def update(self, trigger=True, **kwargs):
for (param, const) in zip(params, constants):
param.constant = const

def update(self, trigger=True, **kwargs):
"""
The update method updates the stream parameters in response to
some event. If the stream has a custom transform method, this
is applied to transform the parameter values accordingly.
If trigger is enabled, the trigger classmethod is invoked on
this particular Stream instance.
"""
self._set_stream_parameters(**kwargs)
transformed = self.transform()
if transformed:
self._set_stream_parameters(**transformed)
if trigger:
self.trigger([self])

Expand All @@ -196,10 +181,11 @@ def __repr__(self):
cls_name = self.__class__.__name__
kwargs = ','.join('%s=%r' % (k,v)
for (k,v) in self.get_param_values() if k != 'name')
if not self.preprocessors:
if not self._rename:
return '%s(%s)' % (cls_name, kwargs)
else:
return '%s(%r, %s)' % (cls_name, self.preprocessors, kwargs)
return '%s(%r, %s)' % (cls_name, self._rename, kwargs)



def __str__(self):
Expand Down Expand Up @@ -276,9 +262,16 @@ class PlotSize(Stream):
Returns the dimensions of a plot once it has been displayed.
"""

width = param.Integer(300, doc="The width of the plot in pixels")
width = param.Integer(300, constant=True, doc="The width of the plot in pixels")

height = param.Integer(300, doc="The height of the plot in pixels")
height = param.Integer(300, constant=True, doc="The height of the plot in pixels")

scale = param.Number(default=1.0, constant=True, doc="""
Scale factor to scale width and height values reported by the stream""")

def transform(self):
return {'width': int(self.width * self.scale),
'height': int(self.height * self.scale)}


class RangeXY(Stream):
Expand Down Expand Up @@ -327,7 +320,7 @@ class Selection1D(Stream):
A stream representing a 1D selection of objects by their index.
"""

index = param.List(default=[], doc="""
index = param.List(default=[], constant=True, doc="""
Indices into a 1D datastructure.""")


Expand All @@ -353,9 +346,6 @@ def contents(self):
for k in self._obj.params().keys() if k!= 'name'}
else:
remapped={k:v for k,v in self._obj.get_param_values() if k!= 'name'}

for preprocessor in self.preprocessors:
remapped = preprocessor(remapped)
return remapped


Expand Down
70 changes: 61 additions & 9 deletions tests/teststreams.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
"""
import param
from holoviews.element.comparison import ComparisonTestCase
from holoviews.streams import Stream, PositionX, PositionY, PositionXY, ParamValues
from holoviews.streams import Rename, Group
from holoviews.streams import * # noqa (Test all available streams)

def test_all_stream_parameters_constant():
all_stream_cls = [v for v in globals().values() if
isinstance(v, type) and issubclass(v, Stream)]
for stream_cls in all_stream_cls:
for name, param in stream_cls.params().items():
if param.constant != True:
raise TypeError('Parameter %s of stream %s not declared constant'
% (name, stream_cls.__name__))

class TestSubscriber(object):

Expand Down Expand Up @@ -138,12 +145,57 @@ def test_batch_subscribers(self):
self.assertEqual(subscriber2.call_count, 1)


class TestPreprocessors(ComparisonTestCase):
class TestParameterRenaming(ComparisonTestCase):

def test_rename_preprocessor(self):
position = PositionXY([Rename(x='x1',y='y1')], x=1, y=3)
self.assertEqual(position.contents, dict(x1=1, y1=3))
def test_simple_rename_constructor(self):
xy = PositionXY(rename={'x':'xtest', 'y':'ytest'}, x=0, y=4)
self.assertEqual(xy.contents, {'xtest':0, 'ytest':4})

def test_invalid_rename_constructor(self):
with self.assertRaises(KeyError) as cm:
PositionXY(rename={'x':'xtest', 'z':'ytest'}, x=0, y=4)
self.assertEqual(str(cm).endswith('is not a stream parameter'), True)

def test_clashing_rename_constructor(self):
with self.assertRaises(KeyError) as cm:
PositionXY(rename={'x':'xtest', 'y':'x'}, x=0, y=4)
self.assertEqual(str(cm).endswith('parameter of the same name'), True)

def test_simple_rename_method(self):
xy = PositionXY(x=0, y=4)
renamed = xy.rename(x='xtest', y='ytest')
self.assertEqual(renamed.contents, {'xtest':0, 'ytest':4})

def test_invalid_rename_method(self):
xy = PositionXY(x=0, y=4)
with self.assertRaises(KeyError) as cm:
renamed = xy.rename(x='xtest', z='ytest')
self.assertEqual(str(cm).endswith('is not a stream parameter'), True)

def test_clashing_rename_method(self):
xy = PositionXY(x=0, y=4)
with self.assertRaises(KeyError) as cm:
renamed = xy.rename(x='xtest', y='x')
self.assertEqual(str(cm).endswith('parameter of the same name'), True)


class TestPlotSizeTransform(ComparisonTestCase):

def test_plotsize_initial_contents_1(self):
plotsize = PlotSize(width=300, height=400, scale=0.5)
self.assertEqual(plotsize.contents, {'width':300, 'height':400, 'scale':0.5})

def test_plotsize_update_1(self):
plotsize = PlotSize(scale=0.5)
plotsize.update(width=300, height=400)
self.assertEqual(plotsize.contents, {'width':150, 'height':200, 'scale':0.5})

def test_plotsize_initial_contents_2(self):
plotsize = PlotSize(width=600, height=100, scale=2)
self.assertEqual(plotsize.contents, {'width':600, 'height':100, 'scale':2})

def test_plotsize_update_2(self):
plotsize = PlotSize(scale=2)
plotsize.update(width=600, height=100)
self.assertEqual(plotsize.contents, {'width':1200, 'height':200, 'scale':2})

def test_group_preprocessor(self):
position = PositionXY([Group('mygroup')], x=1, y=3)
self.assertEqual(position.contents, dict(mygroup={'x':1,'y':3}))

0 comments on commit 25fde35

Please sign in to comment.