diff --git a/param/parameters.py b/param/parameters.py index 504cab35..ba4f96ab 100644 --- a/param/parameters.py +++ b/param/parameters.py @@ -1226,6 +1226,9 @@ def _validate_value(self, value: t.Any, allow_None: bool) -> None: if self.allow_None and value is None: return + if callable(value) and not inspect.isgeneratorfunction(value): + return + if not isinstance(value, _dt_types) and not (allow_None and value is None): raise ValueError( f"{_validate_error_prefix(self)} only takes datetime and " @@ -1324,6 +1327,9 @@ def _validate_value(self, value, allow_None): if self.allow_None and value is None: return + if callable(value) and not inspect.isgeneratorfunction(value): + return + if (not isinstance(value, dt.date) or isinstance(value, dt.datetime)) and not (allow_None and value is None): raise ValueError( f"{_validate_error_prefix(self)} only takes date types." diff --git a/tests/testdynamicparams.py b/tests/testdynamicparams.py index bf511089..6c62e89a 100644 --- a/tests/testdynamicparams.py +++ b/tests/testdynamicparams.py @@ -250,6 +250,50 @@ def test_dynamic_shared_numbergen(self): self.assertNotEqual(call_1, t12.x) +class TestDynamicCallableAcrossNumberSiblings(unittest.TestCase): + """ + Number, Integer, Date and CalendarDate all subclass Dynamic, whose + contract is that any such parameter may be set to a callable that is + invoked to produce the value on get. Every sibling must accept a callable + uniformly; Date and CalendarDate used to reject it at validation time. + """ + + def setUp(self): + super().setUp() + param.Dynamic.time_dependent = False + + import datetime as dt + + self.cases = [ + (param.Number, lambda: 5, 5), + (param.Integer, lambda: 5, 5), + (param.Date, lambda: dt.datetime(2021, 5, 5), dt.datetime(2021, 5, 5)), + (param.CalendarDate, lambda: dt.date(2021, 5, 5), dt.date(2021, 5, 5)), + ] + + def test_set_callable_and_get_resolves(self): + for ptype, gen, expected in self.cases: + with self.subTest(ptype=ptype.__name__): + + class P(param.Parameterized): + v = ptype() + + p = P() + # Setting a callable must not raise for any Dynamic sibling. + p.v = gen + # Getting resolves the dynamic value by calling the callable. + self.assertEqual(p.v, expected) + + def test_callable_as_constructor_default(self): + for ptype, gen, expected in self.cases: + with self.subTest(ptype=ptype.__name__): + + class P(param.Parameterized): + v = ptype(default=gen) + + self.assertEqual(P().v, expected) + + # Commented out block in the original doctest version. # Maybe these are features originally planned but never implemented