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

Preprocessor refactor #1232

Merged
merged 15 commits into from Mar 29, 2017
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
132 changes: 58 additions & 74 deletions holoviews/streams.py
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,7 +61,7 @@ def trigger(cls, streams):
stream.deactivate()


def __init__(self, preprocessors=[], source=None, subscribers=[],
def __init__(self, rename={}, source=None, subscribers=[],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did none of the docstrings mention preprocessors? Either way docstrings should now mention the rename argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 65131a2

linked=True, **params):
"""
Mapping allows multiple streams with similar event state to be
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,23 @@ 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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docstring.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 65131a2

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 +127,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 +154,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 +175,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 +256,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, constant=True, doc="The height of the plot in pixels")

height = param.Integer(300, 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 +314,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 +340,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
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}))