Skip to content

Commit

Permalink
Merge 2b1356b into 0214f41
Browse files Browse the repository at this point in the history
  • Loading branch information
jlstevens committed Apr 24, 2017
2 parents 0214f41 + 2b1356b commit 6e1ae1b
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 43 deletions.
9 changes: 5 additions & 4 deletions holoviews/core/spaces.py
Expand Up @@ -15,7 +15,7 @@
from .ndmapping import UniformNdMapping, NdMapping, item_check
from .overlay import Overlay, CompositeOverlay, NdOverlay, Overlayable
from .options import Store, StoreOptions
from ..streams import Stream, Next
from ..streams import Stream

class HoloMap(UniformNdMapping, Overlayable):
"""
Expand Down Expand Up @@ -652,9 +652,10 @@ def __init__(self, callback, initial_items=None, **params):
if self.kdims:
raise Exception(prefix + ' must be declared without key dimensions')
if len(self.streams)> 1:
raise Exception(prefix + ' must have either streams=[] or streams=[Next()]')
if len(self.streams) == 1 and not isinstance(self.streams[0], Next):
raise Exception(prefix + ' can only accept a single Next() stream')
raise Exception(prefix + ' must have either streams=[] or a single, '
+ 'stream instance without any stream parameters')
if util.stream_parameters(self.streams) != []:
raise Exception(prefix + ' cannot accept any stream parameters')

self._posarg_keys = util.validate_dynamic_argspec(self.callback.argspec,
self.kdims,
Expand Down
77 changes: 39 additions & 38 deletions holoviews/streams.py
Expand Up @@ -5,6 +5,7 @@
"""

import param
import numpy as np
from numbers import Number
from collections import defaultdict
from .core import util
Expand Down Expand Up @@ -71,6 +72,44 @@ class Stream(param.Parameterized):
# e.g. Stream._callbacks['bokeh'][Stream] = Callback
_callbacks = defaultdict(dict)


@classmethod
def define(cls, name, **kwargs):
"""
Utility to quickly and easily declare Stream classes.
Takes a stream class name and a set of keywords where each
keyword becomes a parameter. If the value is already a
parameter, it is simply used otherwise the appropriate parameter
type is inferred and declared, using the value as the default.
"""
params = {'name':param.String(default=name)}
for k,v in kwargs.items():
if isinstance(v, param.Parameter):
params[k] = v
elif isinstance(v, bool):
params[k] = param.Boolean(default=v)
elif isinstance(v, int):
params[k] = param.Integer(default=v)
elif isinstance(v, float):
params[k] = param.Number(default=v)
elif isinstance(v,str):
params[k] = param.String(default=v)
elif isinstance(v,dict):
params[k] = param.Dict(default=v)
elif isinstance(v, tuple):
params[k] = param.Tuple(default=v)
elif isinstance(v,list):
params[k] = param.List(default=v)
elif isinstance(v,np.ndarray):
params[k] = param.Array(default=v)
else:
params[k] = param.Parameter(default=v)

# Dynamic class creation using type
return type(name, (Stream,), params)


@classmethod
def trigger(cls, streams):
"""
Expand Down Expand Up @@ -295,14 +334,6 @@ def __str__(self):
return repr(self)


class Next(Stream):
"""
Next is a special stream used to trigger generators. It may also be
used to trigger DynamicMaps using callables with no arguments.
"""
pass


class Counter(Stream):
"""
Simple stream that automatically increments an integer counter
Expand All @@ -315,36 +346,6 @@ def transform(self):
return {'counter': self.counter + 1}


class X(Stream):
"""
Simple numeric stream representing a position along the x-axis.
"""

x = param.Number(default=0, constant=True, doc="""
Numeric position along the x-axis.""")


class Y(Stream):
"""
Simple numeric stream representing a position along the y-axis.
"""

y = param.Number(default=0, constant=True, doc="""
Numeric position along the y-axis.""")


class XY(Stream):
"""
Simple numeric stream representing a position along the x- and y-axes.
"""

x = param.Number(default=0, constant=True, doc="""
Numeric position along the x-axis.""")

y = param.Number(default=0, constant=True, doc="""
Numeric position along the y-axis.""")


class LinkedStream(Stream):
"""
A LinkedStream indicates is automatically linked to plot interactions
Expand Down
5 changes: 4 additions & 1 deletion tests/testdynamic.py
Expand Up @@ -4,10 +4,13 @@
from holoviews import Dimension, NdLayout, GridSpace, Layout
from holoviews.core.spaces import DynamicMap, HoloMap, Callable
from holoviews.element import Image, Scatter, Curve, Text, Points
from holoviews.streams import XY, PointerXY, PointerX, PointerY
from holoviews.streams import Stream, PointerXY, PointerX, PointerY
from holoviews.util import Dynamic
from holoviews.element.comparison import ComparisonTestCase


XY = Stream.define('XY', x=0,y=0)

frequencies = np.linspace(0.5,2.0,5)
phases = np.linspace(0, np.pi*2, 5)
x,y = np.mgrid[-5:6, -5:6] * 0.1
Expand Down
80 changes: 80 additions & 0 deletions tests/teststreams.py
Expand Up @@ -15,6 +15,86 @@ def test_all_stream_parameters_constant():
raise TypeError('Parameter %s of stream %s not declared constant'
% (name, stream_cls.__name__))


class TestStreamsDefine(ComparisonTestCase):

def setUp(self):
self.XY = Stream.define('XY', x=0.0, y=5.0)
self.TypesTest = Stream.define('TypesTest',
t=True,
u=0,
v=1.2,
w= (1,'a'),
x='string',
y= [],
z = np.array([1,2,3]))

test_param = param.Integer(default=42, doc='Test docstring')
self.ExplicitTest = Stream.define('ExplicitTest',
test=test_param)

def test_XY_types(self):
self.assertEqual(isinstance(self.XY.params('x'), param.Number),True)
self.assertEqual(isinstance(self.XY.params('y'), param.Number),True)

def test_XY_defaults(self):
self.assertEqual(self.XY.params('x').default,0.0)
self.assertEqual(self.XY.params('y').default, 5.0)

def test_XY_instance(self):
xy = self.XY(x=1,y=2)
self.assertEqual(xy.x, 1)
self.assertEqual(xy.y, 2)

def test_XY_set_invalid_class_x(self):
regexp = "Parameter 'x' only takes numeric values"
with self.assertRaisesRegexp(ValueError, regexp):
self.XY.x = 'string'

def test_XY_set_invalid_class_y(self):
regexp = "Parameter 'y' only takes numeric values"
with self.assertRaisesRegexp(ValueError, regexp):
self.XY.y = 'string'

def test_XY_set_invalid_instance_x(self):
xy = self.XY(x=1,y=2)
regexp = "Parameter 'x' only takes numeric values"
with self.assertRaisesRegexp(ValueError, regexp):
xy.x = 'string'

def test_XY_set_invalid_instance_y(self):
xy = self.XY(x=1,y=2)
regexp = "Parameter 'y' only takes numeric values"
with self.assertRaisesRegexp(ValueError, regexp):
xy.y = 'string'

def test_XY_subscriber_triggered(self):

class Inner(object):
def __init__(self): self.state=None
def __call__(self, x,y): self.state=(x,y)

inner = Inner()
xy = self.XY(x=1,y=2)
xy.add_subscriber(inner)
xy.event(x=42,y=420)
self.assertEqual(inner.state, (42,420))

def test_custom_types(self):
self.assertEqual(isinstance(self.TypesTest.params('t'), param.Boolean),True)
self.assertEqual(isinstance(self.TypesTest.params('u'), param.Integer),True)
self.assertEqual(isinstance(self.TypesTest.params('v'), param.Number),True)
self.assertEqual(isinstance(self.TypesTest.params('w'), param.Tuple),True)
self.assertEqual(isinstance(self.TypesTest.params('x'), param.String),True)
self.assertEqual(isinstance(self.TypesTest.params('y'), param.List),True)
self.assertEqual(isinstance(self.TypesTest.params('z'), param.Array),True)

def test_explicit_parameter(self):
self.assertEqual(isinstance(self.ExplicitTest.params('test'), param.Integer),True)
self.assertEqual(self.ExplicitTest.params('test').default,42)
self.assertEqual(self.ExplicitTest.params('test').doc, 'Test docstring')


class TestSubscriber(object):

def __init__(self):
Expand Down

0 comments on commit 6e1ae1b

Please sign in to comment.