Skip to content

Commit

Permalink
Defined namespace and implementation on Parameters object (#230)
Browse files Browse the repository at this point in the history
Moves most of the code of `Parameterized` onto a `param` object of type `Parameters`, which also acts as a namespace. The goal is to make this the face of the public API, cleaning up the namespace of user classes.

The new API is likely to change as we clean it up, but the previous API remains available, so existing code should not be affected. The previous API will be deprecated at some point in the future; `Parameters._disable_stubs` can be used to control deprecation warnings/errors/silence.

Docstrings of methods on Parameterized that have moved to Parameters point readers to the new methods. 

Existing tests have been copied into an `API0` subdirectory, while tests in `API1` use the Parameters namespace. (This wholesale copying could probably be improved in the future.)

Ideally, `dir()` would already return only the cleaned up namespace (i.e. not the deprecated methods on `Parameterized`), but this has not been done yet and may not be easy.
  • Loading branch information
jlstevens authored and ceball committed Jun 25, 2018
1 parent 67bbf36 commit 161ebc5
Show file tree
Hide file tree
Showing 40 changed files with 2,945 additions and 858 deletions.
8 changes: 4 additions & 4 deletions numbergen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def _rational(self, val):
elif hasattr(val, 'numer'):
(numer, denom) = (int(val.numer()), int(val.denom()))
else:
param.main.warning("Casting type '%s' to Fraction.fraction"
param.main.param.warning("Casting type '%s' to Fraction.fraction"
% type(val).__name__)
frac = fractions.Fraction(str(val))
numer, denom = frac.numerator, frac.denominator
Expand Down Expand Up @@ -342,10 +342,10 @@ def _verify_constrained_hash(self):
"""
Warn if the object name is not explicitly set.
"""
changed_params = dict(self.get_param_values(onlychanged=True))
changed_params = dict(self.param.get_param_values(onlychanged=True))
if self.time_dependent and ('name' not in changed_params):
self.warning("Default object name used to set the seed: "
"random values conditional on object instantiation order.")
self.param.warning("Default object name used to set the seed: "
"random values conditional on object instantiation order.")

def _hash_and_seed(self):
"""
Expand Down
2 changes: 1 addition & 1 deletion param/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def __call__(self, val=None, time_type=None):
if time_type and val is None:
raise Exception("Please specify a value for the new time_type.")
if time_type:
type_param = self.params('time_type')
type_param = self.param.params('time_type')
type_param.constant = False
self.time_type = time_type
type_param.constant = True
Expand Down
6 changes: 3 additions & 3 deletions param/ipython.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ def get_param_info(self, obj, include_super=True):
True, parameters are also collected from the super classes.
"""

params = dict(obj.params())
params = dict(obj.param.params())
if isinstance(obj,type):
changed = []
val_dict = dict((k,p.default) for (k,p) in params.items())
self_class = obj
else:
changed = [name for (name,_) in obj.get_param_values(onlychanged=True)]
val_dict = dict(obj.get_param_values())
changed = [name for (name,_) in obj.param.get_param_values(onlychanged=True)]
val_dict = dict(obj.param.get_param_values())
self_class = obj.__class__

if not include_super:
Expand Down
1,838 changes: 989 additions & 849 deletions param/parameterized.py

Large diffs are not rendered by default.

Empty file added tests/API0/__init__.py
Empty file.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def test_qualify(self):
r)

self.assertEqual(obj.pprint(qualify=True),
"tests.testparameterizedrepr."+r)
"tests.API0.testparameterizedrepr."+r)



Expand Down
File renamed without changes.
File renamed without changes.
14 changes: 14 additions & 0 deletions tests/testtimedependent.py → tests/API0/testtimedependent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import unittest
import param
import numbergen
import copy

from nose.plugins.skip import SkipTest
import fractions
Expand Down Expand Up @@ -124,6 +125,19 @@ class DynamicClass(param.Parameterized):
a = param.Number(default = self.Incrementer())

self.DynamicClass = DynamicClass
self._start_state = copy.copy([param.Dynamic.time_dependent,
numbergen.TimeAware.time_dependent,
param.Dynamic.time_fn,
numbergen.TimeAware.time_fn,
param.random_seed])

def tearDown(self):
param.Dynamic.time_dependent = self._start_state[0]
numbergen.TimeAware.time_dependent = self._start_state[1]
param.Dynamic.time_fn = self._start_state[2]
numbergen.TimeAware.time_fn = self._start_state[3]
param.random_seed = self._start_state[4]


def test_non_time_dependent(self):
"""
Expand Down
10 changes: 10 additions & 0 deletions tests/API1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import param
import unittest

class API1TestCase(unittest.TestCase):

def setUp(self):
param.parameterized.Parameters._disable_stubs = True

def tearDown(self):
param.parameterized.Parameters._disable_stubs = False
66 changes: 66 additions & 0 deletions tests/API1/testclassselector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
Unit test for ClassSelector parameters.
"""


from numbers import Number

import param
from . import API1TestCase


class TestClassSelectorParameters(API1TestCase):

def setUp(self):
super(TestClassSelectorParameters, self).setUp()
class P(param.Parameterized):
e = param.ClassSelector(default=1,class_=int)
f = param.ClassSelector(default=int,class_=Number, is_instance=False)
g = param.ClassSelector(default=1,class_=(int,str))
h = param.ClassSelector(default=int,class_=(int,str), is_instance=False)

self.P = P

def test_single_class_instance_constructor(self):
p = self.P(e=6)
self.assertEqual(p.e, 6)

def test_single_class_instance_error(self):
exception = "Parameter 'e' value must be an instance of int, not 'a'"
with self.assertRaisesRegexp(ValueError, exception):
p = self.P(e='a')

def test_single_class_type_constructor(self):
p = self.P(f=float)
self.assertEqual(p.f, float)

def test_single_class_type_error(self):
exception = "Parameter 'str' must be a subclass of Number, not 'type'"
with self.assertRaisesRegexp(ValueError, exception):
p = self.P(f=str)

def test_multiple_class_instance_constructor1(self):
p = self.P(g=1)
self.assertEqual(p.g, 1)

def test_multiple_class_instance_constructor2(self):
p = self.P(g='A')
self.assertEqual(p.g, 'A')

def test_multiple_class_instance_error(self):
exception = "Parameter 'g' value must be an instance of \(int, str\), not '3.0'"
with self.assertRaisesRegexp(ValueError, exception):
p = self.P(g=3.0)

def test_multiple_class_type_constructor1(self):
p = self.P(h=int)
self.assertEqual(p.h, int)

def test_multiple_class_type_constructor2(self):
p = self.P(h=str)
self.assertEqual(p.h, str)

def test_multiple_class_type_error(self):
exception = "Parameter 'float' must be a subclass of \(int, str\), not 'type'"
with self.assertRaisesRegexp(ValueError, exception):
p = self.P(h=float)
39 changes: 39 additions & 0 deletions tests/API1/testcolorparameter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Unit test for Color parameters.
"""
import param
from . import API1TestCase

class TestColorParameters(API1TestCase):

def test_initialization_invalid_string(self):
try:
class Q(param.Parameterized):
q = param.Color('red')
except ValueError:
pass
else:
raise AssertionError("No exception raised on out-of-bounds date")

def test_set_invalid_string(self):
class Q(param.Parameterized):
q = param.Color()
try:
Q.q = 'red'
except ValueError:
pass
else:
raise AssertionError("No exception raised on out-of-bounds date")

def test_valid_long_hex(self):
class Q(param.Parameterized):
q = param.Color()
Q.q = '#ffffff'
self.assertEqual(Q.q, '#ffffff')

def test_valid_short_hex(self):
class Q(param.Parameterized):
q = param.Color()
Q.q = '#fff'
self.assertEqual(Q.q, '#fff')

100 changes: 100 additions & 0 deletions tests/API1/testcompositeparams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
Unit test for composite parameters.
Originally implemented as doctests in Topographica in the file
testCompositeParameter.txt
"""

import param
from . import API1TestCase

class TestCompositeParameters(API1TestCase):

def setUp(self):
super(TestCompositeParameters, self).setUp()
# initialize a class with a compound parameter
class A(param.Parameterized):
x = param.Number(default=0)
y = param.Number(default=0)
xy = param.Composite(attribs=['x','y'])

self.A = A
self.a = self.A()

class SomeSequence(object):
"Can't use iter with Dynamic (doesn't pickle, doesn't copy)"
def __init__(self,sequence):
self.sequence=sequence
self.index=0
def __call__(self):
val=self.sequence[self.index]
self.index+=1
return val

self.SomeSequence = SomeSequence

def test_initialization(self):
"Make an instance and do default checks"
self.assertEqual(self.a.x, 0)
self.assertEqual(self.a.y, 0)
self.assertEqual(self.a.xy, [0,0])


def test_set_component(self):
self.a.x = 1
self.assertEqual(self.a.xy, [1,0])

def test_set_compound(self):
self.a.xy = (2,3)
self.assertEqual(self.a.x, 2)
self.assertEqual(self.a.y, 3)

def test_compound_class(self):
" Get the compound on the class "
self.assertEqual(self.A.xy, [0,0])

def test_set_compound_class_set(self):
self.A.xy = (5,6)
self.assertEqual(self.A.x, 5)
self.assertEqual(self.A.y, 6)

def test_set_compound_class_instance(self):
self.A.xy = (5,6)
# # Make a new instance
b = self.A()
self.assertEqual(b.x, 5)
self.assertEqual(b.y, 6)

def test_set_compound_class_instance_unchanged(self):
self.a.xy = (2,3)
self.A.xy = (5,6)
self.assertEqual(self.a.x, 2)
self.assertEqual(self.a.y, 3)

def test_composite_dynamic(self):
"""
Check CompositeParameter is ok with Dynamic
CB: this test is really of Parameterized.
"""
a2 = self.A(x=self.SomeSequence([1,2,3]),
y=self.SomeSequence([4,5,6]))

a2.x, a2.y # Call of x and y params
# inspect should not advance numbers
self.assertEqual(a2.param.inspect_value('xy'), [1, 4])

def test_composite_dynamic_generator(self):

a2 = self.A(x=self.SomeSequence([1,2,3]),
y=self.SomeSequence([4,5,6]))

a2.x, a2.y # Call of x and y params
ix,iy = a2.param.get_value_generator('xy')
# get_value_generator() should give the objects
self.assertEqual(ix(), 2)
self.assertEqual(iy(), 5)


if __name__ == "__main__":
import nose
nose.runmodule()
54 changes: 54 additions & 0 deletions tests/API1/testdateparam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Unit test for Date parameters.
"""


import datetime as dt
import param
from . import API1TestCase

class TestDateParameters(API1TestCase):

def test_initialization_out_of_bounds(self):
try:
class Q(param.Parameterized):
q = param.Date(dt.datetime(2017,2,27),
bounds=(dt.datetime(2017,2,1),
dt.datetime(2017,2,26)))
except ValueError:
pass
else:
raise AssertionError("No exception raised on out-of-bounds date")

def test_set_out_of_bounds(self):
class Q(param.Parameterized):
q = param.Date(bounds=(dt.datetime(2017,2,1),
dt.datetime(2017,2,26)))
try:
Q.q = dt.datetime(2017,2,27)
except ValueError:
pass
else:
raise AssertionError("No exception raised on out-of-bounds date")

def test_set_exclusive_out_of_bounds(self):
class Q(param.Parameterized):
q = param.Date(bounds=(dt.datetime(2017,2,1),
dt.datetime(2017,2,26)),
inclusive_bounds=(True, False))
try:
Q.q = dt.datetime(2017,2,26)
except ValueError:
pass
else:
raise AssertionError("No exception raised on out-of-bounds date")

def test_get_soft_bounds(self):
q = param.Date(dt.datetime(2017,2,25),
bounds=(dt.datetime(2017,2,1),
dt.datetime(2017,2,26)),
softbounds=(dt.datetime(2017,2,1),
dt.datetime(2017,2,25)))
self.assertEqual(q.get_soft_bounds(), (dt.datetime(2017,2,1),
dt.datetime(2017,2,25)))

Loading

0 comments on commit 161ebc5

Please sign in to comment.