From fc0a5ce5caf22b351d33c2f915bc133f61a557c0 Mon Sep 17 00:00:00 2001 From: maximlt Date: Mon, 3 Jul 2023 22:41:52 +0200 Subject: [PATCH 1/8] de-unittest and more async tests --- tests/testparamdepends.py | 743 +++++++++++++++++++++++++++----------- 1 file changed, 530 insertions(+), 213 deletions(-) diff --git a/tests/testparamdepends.py b/tests/testparamdepends.py index ee3e214f1..266739b99 100644 --- a/tests/testparamdepends.py +++ b/tests/testparamdepends.py @@ -3,7 +3,6 @@ """ import asyncio -import unittest import param import pytest @@ -12,10 +11,33 @@ def async_executor(func): + # Using nest_asyncio to simplify the async_executor implementation + import nest_asyncio + nest_asyncio.apply() asyncio.run(func()) -class TestDependencyParser(unittest.TestCase): +@pytest.fixture +def use_async_executor(): + param.parameterized.async_executor = async_executor + try: + yield + finally: + param.parameterized.async_executor = None + + +@pytest.fixture +def class_name(request): + if request.param.startswith('AP'): + param.parameterized.async_executor = async_executor + try: + yield request.param + finally: + if request.param.startswith('AP'): + param.parameterized.async_executor = None + + +class TestDependencyParser: def test_parameter_value(self): obj, attr, what = _parse_dependency_spec('parameter') @@ -54,7 +76,7 @@ def test_sub_subobject_parameter_attribute(self): assert what == 'constant' -class TestParamDependsSubclassing(unittest.TestCase): +class TestParamDependsSubclassing: def test_param_depends_override_depends_subset(self): @@ -157,9 +179,9 @@ def more_test(self): -class TestParamDepends(unittest.TestCase): +class TestParamDepends: - def setUp(self): + def setup_method(self): class P(param.Parameterized): a = param.Parameter() @@ -196,14 +218,59 @@ def nested_attribute(self): def nested(self): self.nested_count += 1 + class AP(param.Parameterized): + a = param.Parameter() + b = param.Parameter() + + single_count = param.Integer() + attr_count = param.Integer() + single_nested_count = param.Integer() + double_nested_count = param.Integer() + nested_attr_count = param.Integer() + nested_count = param.Integer() + + @param.depends('a', watch=True) + async def single_parameter(self): + self.single_count += 1 + + @param.depends('a:constant', watch=True) + async def constant(self): + self.attr_count += 1 + + @param.depends('b.a', watch=True) + async def single_nested(self): + self.single_nested_count += 1 + + @param.depends('b.b.a', watch=True) + async def double_nested(self): + self.double_nested_count += 1 + + @param.depends('b.a:constant', watch=True) + async def nested_attribute(self): + self.nested_attr_count += 1 + + @param.depends('b.param', watch=True) + async def nested(self): + self.nested_count += 1 + + class P2(param.Parameterized): @param.depends(P.param.a) def external_param(self, a): pass + class AP2(param.Parameterized): + + @param.depends(AP.param.a) + async def external_param(self, a): + pass + + self.P = P + self.AP = AP self.P2 = P2 + self.AP2 = AP2 def test_param_depends_on_init(self): class A(param.Parameterized): @@ -243,6 +310,28 @@ def test(self): b.a = A(c=1) assert b.test_count == 0 + @pytest.mark.usefixtures("use_async_executor") + def test_param_nested_depends_value_unchanged_async(self): + class A(param.Parameterized): + + c = param.Parameter() + + d = param.Parameter() + + class B(param.Parameterized): + + a = param.Parameter() + + test_count = param.Integer() + + @param.depends('a.c', 'a.d', watch=True) + async def test(self): + self.test_count += 1 + + b = B(a=A(c=1)) + b.a = A(c=1) + assert b.test_count == 0 + def test_param_nested_at_class_definition(self): class A(param.Parameterized): @@ -275,6 +364,39 @@ def test(self): B.a.c = 5 assert b.test_count == 3 + @pytest.mark.usefixtures("use_async_executor") + def test_param_nested_at_class_definition_async(self): + + class A(param.Parameterized): + + c = param.Parameter() + + d = param.Parameter() + + class B(param.Parameterized): + + a = param.Parameter(A()) + + test_count = param.Integer() + + @param.depends('a.c', 'a.d', watch=True) + async def test(self): + self.test_count += 1 + + b = B() + + b.a.c = 1 + assert b.test_count == 1 + + b.a.param.update(c=2, d=1) + assert b.test_count == 2 + + b.a = A() + assert b.test_count == 3 + + B.a.c = 5 + assert b.test_count == 3 + def test_param_nested_depends_expands(self): class A(param.Parameterized): @@ -296,6 +418,28 @@ def test(self): b.a = A(c=1, name='A') assert b.test_count == 0 + @pytest.mark.usefixtures("use_async_executor") + def test_param_nested_depends_expands_async(self): + class A(param.Parameterized): + + c = param.Parameter() + + d = param.Parameter() + + class B(param.Parameterized): + + a = param.Parameter() + + test_count = param.Integer() + + @param.depends('a.param', watch=True) + async def test(self): + self.test_count += 1 + + b = B(a=A(c=1, name='A')) + b.a = A(c=1, name='A') + assert b.test_count == 0 + def test_param_depends_class_default_dynamic(self): class A(param.Parameterized): @@ -318,77 +462,105 @@ def nested(self): b.a = A() assert b.nested_count == 2 - def test_param_instance_depends_dynamic_single_nested(self): - inst = self.P() + @pytest.mark.usefixtures("use_async_executor") + def test_param_depends_class_default_dynamic_async(self): + + class A(param.Parameterized): + c = param.Parameter() + + class B(param.Parameterized): + a = param.Parameter(A()) + + nested_count = param.Integer() + + @param.depends('a.c', watch=True) + async def nested(self): + self.nested_count += 1 + + b = B() + + b.a.c = 1 + assert b.nested_count == 1 + + b.a = A() + assert b.nested_count == 2 + + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_single_nested(self, class_name): + inst = getattr(self, class_name)() + # inst = self.P() pinfos = inst.param.method_dependencies('single_nested', intermediate=True) - self.assertEqual(len(pinfos), 0) + assert len(pinfos) == 0 - inst.b = self.P() + inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('single_nested', intermediate=True) - self.assertEqual(len(pinfos), 2) + assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls is getattr(self, class_name) + assert pinfo.inst is inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'a')] - self.assertIs(pinfo2.cls, self.P) - self.assertIs(pinfo2.inst, inst.b) - self.assertEqual(pinfo2.name, 'a') - self.assertEqual(pinfo2.what, 'value') + assert pinfo2.cls is getattr(self, class_name) + assert pinfo2.inst is inst.b + assert pinfo2.name == 'a' + assert pinfo2.what == 'value' assert inst.single_nested_count == 1 inst.b.a = 1 assert inst.single_nested_count == 2 - def test_param_instance_depends_dynamic_single_nested_initialized_no_intermediates(self): - init_b = self.P() - inst = self.P(b=init_b) + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_single_nested_initialized_no_intermediates(self, class_name): + init_b = getattr(self, class_name)() + inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('single_nested', intermediate=False) - self.assertEqual(len(pinfos), 1) + assert len(pinfos) == 1 assert pinfos[0].inst is init_b assert pinfos[0].name == 'a' - new_b = self.P() + new_b = getattr(self, class_name)() inst.b = new_b pinfos = inst.param.method_dependencies('single_nested', intermediate=False) - self.assertEqual(len(pinfos), 1) + assert len(pinfos) == 1 assert pinfos[0].inst is new_b assert pinfos[0].name == 'a' - def test_param_instance_depends_dynamic_single_nested_initialized_only_intermediates(self): - init_b = self.P() - inst = self.P(b=init_b) + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_single_nested_initialized_only_intermediates(self, class_name): + init_b = getattr(self, class_name)() + inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('single_nested', intermediate='only') - self.assertEqual(len(pinfos), 1) + assert len(pinfos) == 1 assert pinfos[0].inst is inst assert pinfos[0].name == 'b' - def test_param_instance_depends_dynamic_single_nested_initialized(self): - init_b = self.P() - inst = self.P(b=init_b) + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_single_nested_initialized(self, class_name): + init_b = getattr(self, class_name)() + inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('single_nested', intermediate=True) - self.assertEqual(len(pinfos), 2) + assert len(pinfos) == 2 - inst.b = self.P() + inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('single_nested', intermediate=True) - self.assertEqual(len(pinfos), 2) + assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + pinfo.cls is getattr(self, class_name) + pinfo.inst is inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'a')] - self.assertIs(pinfo2.cls, self.P) - self.assertIs(pinfo2.inst, inst.b) - self.assertEqual(pinfo2.name, 'a') - self.assertEqual(pinfo2.what, 'value') + pinfo2.cls is getattr(self, class_name) + pinfo2.inst is inst.b + assert pinfo2.name == 'a' + assert pinfo2.what == 'value' assert inst.single_nested_count == 0 @@ -399,30 +571,31 @@ def test_param_instance_depends_dynamic_single_nested_initialized(self): init_b.a = 2 assert inst.single_nested_count == 1 - def test_param_instance_depends_dynamic_double_nested(self): - inst = self.P() + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_double_nested(self, class_name): + inst = getattr(self, class_name)() pinfos = inst.param.method_dependencies('double_nested', intermediate=True) - self.assertEqual(len(pinfos), 0) + assert len(pinfos) == 0 - inst.b = self.P(b=self.P()) + inst.b = getattr(self, class_name)(b=getattr(self, class_name)()) pinfos = inst.param.method_dependencies('double_nested', intermediate=True) - self.assertEqual(len(pinfos), 3) + assert len(pinfos) == 3 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls is getattr(self, class_name) + assert pinfo.inst is inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'b')] - self.assertIs(pinfo2.cls, self.P) - self.assertIs(pinfo2.inst, inst.b) - self.assertEqual(pinfo2.name, 'b') - self.assertEqual(pinfo2.what, 'value') + assert pinfo2.cls is getattr(self, class_name) + assert pinfo2.inst is inst.b + assert pinfo2.name == 'b' + assert pinfo2.what == 'value' pinfo3 = pinfos[(inst.b.b, 'a')] - self.assertIs(pinfo3.cls, self.P) - self.assertIs(pinfo3.inst, inst.b.b) - self.assertEqual(pinfo3.name, 'a') - self.assertEqual(pinfo3.what, 'value') + assert pinfo3.cls is getattr(self, class_name) + assert pinfo3.inst is inst.b.b + assert pinfo3.name == 'a' + assert pinfo3.what == 'value' assert inst.double_nested_count == 1 @@ -430,146 +603,151 @@ def test_param_instance_depends_dynamic_double_nested(self): assert inst.double_nested_count == 2 old_subobj = inst.b.b - inst.b.b = self.P(a=3) + inst.b.b = getattr(self, class_name)(a=3) assert inst.double_nested_count == 3 old_subobj.a = 4 assert inst.double_nested_count == 3 - inst.b.b = self.P(a=3) + inst.b.b = getattr(self, class_name)(a=3) assert inst.double_nested_count == 3 inst.b.b.a = 4 assert inst.double_nested_count == 4 - inst.b.b = self.P(a=3) + inst.b.b = getattr(self, class_name)(a=3) assert inst.double_nested_count == 5 - def test_param_instance_depends_dynamic_double_nested_partially_initialized(self): - inst = self.P(b=self.P()) + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_double_nested_partially_initialized(self, class_name): + inst = getattr(self, class_name)(b=getattr(self, class_name)()) pinfos = inst.param.method_dependencies('double_nested', intermediate=True) - self.assertEqual(len(pinfos), 2) + assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls == getattr(self, class_name) + assert pinfo.inst == inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' pinfo = pinfos[(inst.b, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst.b) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls == getattr(self, class_name) + assert pinfo.inst == inst.b + assert pinfo.name == 'b' + assert pinfo.what == 'value' - inst.b.b = self.P() + inst.b.b = getattr(self, class_name)() assert inst.double_nested_count == 1 inst.b.b.a = 1 assert inst.double_nested_count == 2 - def test_param_instance_depends_dynamic_nested_attribute(self): - inst = self.P() + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_nested_attribute(self, class_name): + inst = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested_attribute', intermediate=True) - self.assertEqual(len(pinfos), 0) + assert len(pinfos) == 0 - inst.b = self.P() + inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested_attribute', intermediate=True) - self.assertEqual(len(pinfos), 2) + assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls == getattr(self, class_name) + assert pinfo.inst == inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'a')] - self.assertIs(pinfo2.cls, self.P) - self.assertIs(pinfo2.inst, inst.b) - self.assertEqual(pinfo2.name, 'a') - self.assertEqual(pinfo2.what, 'constant') + assert pinfo2.cls == getattr(self, class_name) + assert pinfo2.inst == inst.b + assert pinfo2.name == 'a' + assert pinfo2.what == 'constant' assert inst.nested_attr_count == 1 inst.b.param.a.constant = True assert inst.nested_attr_count == 2 - new_b = self.P() + new_b = getattr(self, class_name)() new_b.param.a.constant = True inst.b = new_b assert inst.nested_attr_count == 2 - def test_param_instance_depends_dynamic_nested_attribute_initialized(self): - inst = self.P(b=self.P()) + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_nested_attribute_initialized(self, class_name): + inst = getattr(self, class_name)(b=getattr(self, class_name)()) pinfos = inst.param.method_dependencies('nested_attribute', intermediate=True) - self.assertEqual(len(pinfos), 2) + assert len(pinfos) == 2 - inst.b = self.P() + inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested_attribute', intermediate=True) - self.assertEqual(len(pinfos), 2) + assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls == getattr(self, class_name) + assert pinfo.inst == inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'a')] - self.assertIs(pinfo2.cls, self.P) - self.assertIs(pinfo2.inst, inst.b) - self.assertEqual(pinfo2.name, 'a') - self.assertEqual(pinfo2.what, 'constant') + assert pinfo2.cls == getattr(self, class_name) + assert pinfo2.inst == inst.b + assert pinfo2.name == 'a' + assert pinfo2.what == 'constant' assert inst.nested_attr_count == 0 inst.b.param.a.constant = True assert inst.nested_attr_count == 1 - def test_param_instance_depends_dynamic_nested(self): - inst = self.P() + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_nested(self, class_name): + inst = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested') - self.assertEqual(len(pinfos), 0) + assert len(pinfos) == 0 - inst.b = self.P() + inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested') - self.assertEqual(len(pinfos), 10) + assert len(pinfos) == 10 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls is getattr(self, class_name) + assert pinfo.inst is inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' for p in ['a', 'b', 'name', 'nested_count', 'single_count', 'attr_count']: pinfo2 = pinfos[(inst.b, p)] - self.assertIs(pinfo2.cls, self.P) - self.assertIs(pinfo2.inst, inst.b) - self.assertEqual(pinfo2.name, p) - self.assertEqual(pinfo2.what, 'value') + assert pinfo2.cls is getattr(self, class_name) + assert pinfo2.inst is inst.b + assert pinfo2.name == p + assert pinfo2.what == 'value' assert inst.nested_count == 1 inst.b.a = 1 assert inst.nested_count == 3 - def test_param_instance_depends_dynamic_nested_initialized(self): - init_b = self.P() - inst = self.P(b=init_b) + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_nested_initialized(self, class_name): + init_b = getattr(self, class_name)() + inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('nested') - self.assertEqual(len(pinfos), 10) + assert len(pinfos) == 10 - inst.b = self.P() + inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested') - self.assertEqual(len(pinfos), 10) + assert len(pinfos) == 10 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls is getattr(self, class_name) + assert pinfo.inst is inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' for p in ['a', 'b', 'name', 'nested_count', 'single_count', 'attr_count']: pinfo2 = pinfos[(inst.b, p)] - self.assertIs(pinfo2.cls, self.P) - self.assertIs(pinfo2.inst, inst.b) - self.assertEqual(pinfo2.name, p) - self.assertEqual(pinfo2.what, 'value') + assert pinfo2.cls is getattr(self, class_name) + assert pinfo2.inst is inst.b + assert pinfo2.name == p + assert pinfo2.what == 'value' assert inst.single_nested_count == 0 @@ -580,27 +758,28 @@ def test_param_instance_depends_dynamic_nested_initialized(self): init_b.a = 2 assert inst.single_nested_count == 1 - def test_param_instance_depends_dynamic_nested_changed_value(self): - init_b = self.P(a=1) - inst = self.P(b=init_b) + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends_dynamic_nested_changed_value(self, class_name): + init_b = getattr(self, class_name)(a=1) + inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('nested') - self.assertEqual(len(pinfos), 10) + assert len(pinfos) == 10 - inst.b = self.P(a=2) + inst.b = getattr(self, class_name)(a=2) pinfos = inst.param.method_dependencies('nested') - self.assertEqual(len(pinfos), 10) + assert len(pinfos) == 10 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls is getattr(self, class_name) + assert pinfo.inst is inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' for p in ['a', 'b', 'name', 'nested_count', 'single_count', 'attr_count']: pinfo2 = pinfos[(inst.b, p)] - self.assertIs(pinfo2.cls, self.P) - self.assertIs(pinfo2.inst, inst.b) - self.assertEqual(pinfo2.name, p) - self.assertEqual(pinfo2.what, 'value') + assert pinfo2.cls is getattr(self, class_name) + assert pinfo2.inst is inst.b + assert pinfo2.name == p + assert pinfo2.what == 'value' assert inst.single_nested_count == 1 @@ -611,77 +790,79 @@ def test_param_instance_depends_dynamic_nested_changed_value(self): init_b.a = 2 assert inst.single_nested_count == 2 - def test_param_instance_depends(self): - p = self.P() + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_instance_depends(self, class_name): + p = getattr(self, class_name)() pinfos = p.param.method_dependencies('single_parameter') - self.assertEqual(len(pinfos), 1) + assert len(pinfos) == 1 pinfo = pinfos[0] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, p) - self.assertEqual(pinfo.name, 'a') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls is getattr(self, class_name) + assert pinfo.inst is p + assert pinfo.name == 'a' + assert pinfo.what == 'value' p.a = 1 assert p.single_count == 1 - def test_param_class_depends(self): - pinfos = self.P.param.method_dependencies('single_parameter') - self.assertEqual(len(pinfos), 1) + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_class_depends(self, class_name): + pinfos = getattr(self, class_name).param.method_dependencies('single_parameter') + assert len(pinfos) == 1 pinfo = pinfos[0] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, None) - self.assertEqual(pinfo.name, 'a') - self.assertEqual(pinfo.what, 'value') - - def test_param_class_depends_constant(self): - pinfos = self.P.param.method_dependencies('constant') - self.assertEqual(len(pinfos), 1) + assert pinfo.cls is getattr(self, class_name) + assert pinfo.inst is None + assert pinfo.name == 'a' + assert pinfo.what == 'value' + + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_class_depends_constant(self, class_name): + pinfos = getattr(self, class_name).param.method_dependencies('constant') + assert len(pinfos) == 1 pinfo = pinfos[0] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, None) - self.assertEqual(pinfo.name, 'a') - self.assertEqual(pinfo.what, 'constant') + assert pinfo.cls is getattr(self, class_name) + assert pinfo.inst is None + assert pinfo.name == 'a' + assert pinfo.what == 'constant' - def test_param_inst_depends_nested(self): - inst = self.P(b=self.P()) + @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) + def test_param_inst_depends_nested(self, class_name): + inst = getattr(self, class_name)(b=getattr(self, class_name)()) pinfos = inst.param.method_dependencies('nested') - self.assertEqual(len(pinfos), 10) + assert len(pinfos) == 10 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, inst) - self.assertEqual(pinfo.name, 'b') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls is getattr(self, class_name) + assert pinfo.inst is inst + assert pinfo.name == 'b' + assert pinfo.what == 'value' for p in ['name', 'a', 'b']: info = pinfos[(inst.b, p)] - self.assertEqual(info.name, p) - self.assertIs(info.inst, inst.b) + assert info.name == p + assert info.inst is inst.b - def test_param_external_param_instance(self): - inst = self.P2() + @pytest.mark.parametrize('class_name', ['P2', 'AP2'], indirect=True) + def test_param_external_param_instance(self, class_name): + inst = getattr(self, class_name)() pinfos = inst.param.method_dependencies('external_param') pinfo = pinfos[0] - self.assertIs(pinfo.cls, self.P) - self.assertIs(pinfo.inst, None) - self.assertEqual(pinfo.name, 'a') - self.assertEqual(pinfo.what, 'value') + assert pinfo.cls is getattr(self, class_name[:-1]) + assert pinfo.inst is None + assert pinfo.name == 'a' + assert pinfo.what == 'value' + @pytest.mark.usefixtures('use_async_executor') def test_async(self): - try: - param.parameterized.async_executor = async_executor - class P(param.Parameterized): - a = param.Parameter() - single_count = param.Integer() - - @param.depends('a', watch=True) - async def single_parameter(self): - self.single_count += 1 - - inst = P() - inst.a = 'test' - assert inst.single_count == 1 - finally: - param.parameterized.async_executor = None + class P(param.Parameterized): + a = param.Parameter() + single_count = param.Integer() + + @param.depends('a', watch=True) + async def single_parameter(self): + self.single_count += 1 + + inst = P() + inst.a = 'test' + assert inst.single_count == 1 def test_param_depends_on_parameterized_attribute(self): # Issue https://github.com/holoviz/param/issues/635 @@ -707,6 +888,31 @@ def cb(self): assert not called + @pytest.mark.usefixtures('use_async_executor') + def test_param_depends_on_parameterized_attribute_async(self): + # Issue https://github.com/holoviz/param/issues/635 + + called = [] + + class Sub(param.Parameterized): + s = param.String() + + class P(param.Parameterized): + test_param = param.Parameter() + + def __init__(self, **params): + self._sub = Sub() + super().__init__(**params) + + @param.depends('_sub.s', watch=True) + async def cb(self): + called.append(1) + + p = P() + p.test_param = 'modified' + + assert not called + def test_param_depends_on_method(self): method_count = 0 @@ -736,6 +942,36 @@ def method2(self): inst.a = 2 assert method_count == 1 + @pytest.mark.usefixtures('use_async_executor') + def test_param_depends_on_method_async(self): + + method_count = 0 + + class A(param.Parameterized): + a = param.Integer() + + @param.depends('a', watch=True) + async def method1(self): + pass + + @param.depends('method1', watch=True) + async def method2(self): + nonlocal method_count + method_count += 1 + + inst = A() + pinfos = inst.param.method_dependencies('method2') + assert len(pinfos) == 1 + + pinfo = pinfos[0] + assert pinfo.cls is A + assert pinfo.inst is inst + assert pinfo.name == 'a' + assert pinfo.what == 'value' + + inst.a = 2 + assert method_count == 1 + def test_param_depends_on_method_subparameter(self): method1_count = 0 @@ -772,6 +1008,42 @@ def method2(self): assert method1_count == 0 assert method2_count == 1 + @pytest.mark.usefixtures('use_async_executor') + def test_param_depends_on_method_subparameter_async(self): + + method1_count = 0 + method2_count = 0 + + class Sub(param.Parameterized): + a = param.Integer() + + @param.depends('a') + async def method1(self): + nonlocal method1_count + method1_count += 1 + + class Main(param.Parameterized): + sub = param.Parameter() + + @param.depends('sub.method1', watch=True) + async def method2(self): + nonlocal method2_count + method2_count += 1 + + sub = Sub() + main = Main(sub=sub) + pinfos = main.param.method_dependencies('method2') + assert len(pinfos) == 1 + + pinfo = pinfos[0] + assert pinfo.cls is Sub + assert pinfo.inst is sub + assert pinfo.name == 'a' + assert pinfo.what == 'value' + + sub.a = 2 + assert method1_count == 0 + assert method2_count == 1 def test_param_depends_on_method_subparameter_after_init(self): # Setup inspired from https://github.com/holoviz/param/issues/764 @@ -820,6 +1092,54 @@ def method1(self): assert method1_count == 0 assert method2_count == 1 + @pytest.mark.usefixtures('use_async_executor') + def test_param_depends_on_method_subparameter_after_init_async(self): + # Setup inspired from https://github.com/holoviz/param/issues/764 + + method1_count = 0 + method2_count = 0 + + class Controls(param.Parameterized): + + explorer = param.Parameter() + + @param.depends('explorer.method1', watch=True) + async def method2(self): + nonlocal method2_count + method2_count += 1 + + + class Explorer(param.Parameterized): + + controls = param.Parameter() + + x = param.Selector(objects=['a', 'b']) + + def __init__(self, **params): + super().__init__(**params) + self.controls = Controls(explorer=self) + + @param.depends('x') + async def method1(self): + nonlocal method1_count + method1_count += 1 + + explorer = Explorer() + + pinfos = explorer.controls.param.method_dependencies('method2') + assert len(pinfos) == 1 + + pinfo = pinfos[0] + assert pinfo.cls is Explorer + assert pinfo.inst is explorer + assert pinfo.name == 'x' + assert pinfo.what == 'value' + + explorer.x = 'b' + + assert method1_count == 0 + assert method2_count == 1 + def test_param_depends_class_with_len(self): # https://github.com/holoviz/param/issues/747 @@ -842,9 +1162,9 @@ def __len__(self): assert count == 1 -class TestParamDependsFunction(unittest.TestCase): +class TestParamDependsFunction: - def setUp(self): + def setup_method(self): class P(param.Parameterized): a = param.Parameter() b = param.Parameter() @@ -865,7 +1185,7 @@ def function(value, c): 'watch': False, 'on_init': False } - self.assertEqual(function._dinfo, dependencies) + assert function._dinfo == dependencies def test_param_depends_function_class_params(self): p = self.P @@ -880,7 +1200,7 @@ def function(value, c): 'watch': False, 'on_init': False } - self.assertEqual(function._dinfo, dependencies) + assert function._dinfo == dependencies def test_param_depends_function_instance_params_watch(self): p = self.P(a=1, b=2) @@ -892,9 +1212,9 @@ def function(value, c): d.append(value+c) p.a = 2 - self.assertEqual(d, [4]) + assert d == [4] p.b = 3 - self.assertEqual(d, [4, 5]) + assert d == [4, 5] def test_param_depends_function_class_params_watch(self): p = self.P @@ -908,26 +1228,23 @@ def function(value, c): d.append(value+c) p.a = 2 - self.assertEqual(d, [4]) + assert d == [4] p.b = 3 - self.assertEqual(d, [4, 5]) + assert d == [4, 5] + @pytest.mark.usefixtures('use_async_executor') def test_async(self): - try: - param.parameterized.async_executor = async_executor - p = self.P(a=1) + p = self.P(a=1) - d = [] + d = [] - @param.depends(p.param.a, watch=True) - async def function(value): - d.append(value) + @param.depends(p.param.a, watch=True) + async def function(value): + d.append(value) - p.a = 2 + p.a = 2 - assert d == [2] - finally: - param.parameterized.async_executor = None + assert d == [2] def test_misspelled_parameter_in_depends(): From c6668a5d6fc53e757db0c89988acb09d0cb4bbfa Mon Sep 17 00:00:00 2001 From: maximlt Date: Mon, 3 Jul 2023 23:12:18 +0200 Subject: [PATCH 2/8] clean up testparamdepends --- tests/testparamdepends.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/testparamdepends.py b/tests/testparamdepends.py index 266739b99..e7aa2f8b3 100644 --- a/tests/testparamdepends.py +++ b/tests/testparamdepends.py @@ -488,7 +488,6 @@ async def nested(self): @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) def test_param_instance_depends_dynamic_single_nested(self, class_name): inst = getattr(self, class_name)() - # inst = self.P() pinfos = inst.param.method_dependencies('single_nested', intermediate=True) assert len(pinfos) == 0 From 6792d580537d2c53a26ca85fa8bc05f9ddbe3eb5 Mon Sep 17 00:00:00 2001 From: maximlt Date: Mon, 3 Jul 2023 23:42:06 +0200 Subject: [PATCH 3/8] remove closure to support pickling --- param/parameterized.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/param/parameterized.py b/param/parameterized.py index 4cf8c955e..132a9b5f3 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -694,6 +694,21 @@ def _skip_event(*events, **kwargs): return True +# Two callers at the module top level to support pickling. +async def _async_caller(*events, what='value', changed=None, callback=None, function=None): + if callback: + callback(*events) + if not _skip_event or not _skip_event(*events, what=what, changed=changed): + await function() + + +def _sync_caller(*events, what='value', changed=None, callback=None, function=None): + if callback: + callback(*events) + if not _skip_event(*events, what=what, changed=changed): + return function() + + def _m_caller(self, method_name, what='value', changed=None, callback=None): """ Wraps a method call adding support for scheduling a callback @@ -701,17 +716,8 @@ def _m_caller(self, method_name, what='value', changed=None, callback=None): changed but its values have not. """ function = getattr(self, method_name) - if iscoroutinefunction(function): - async def caller(*events): - if callback: - callback(*events) - if not _skip_event or not _skip_event(*events, what=what, changed=changed): - await function() - else: - def caller(*events): - if callback: callback(*events) - if not _skip_event(*events, what=what, changed=changed): - return function() + _caller = _async_caller if iscoroutinefunction(function) else _sync_caller + caller = partial(_caller, what=what, changed=changed, callback=callback, function=function) caller._watcher_name = method_name return caller From c51969de8196a8a506fca7c9746362e09544f80b Mon Sep 17 00:00:00 2001 From: maximlt Date: Mon, 3 Jul 2023 23:42:43 +0200 Subject: [PATCH 4/8] use class if self is None --- param/parameterized.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/param/parameterized.py b/param/parameterized.py index 132a9b5f3..1514106f8 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1704,7 +1704,8 @@ def self_or_cls(self_): def __setstate__(self, state): # Set old parameters state on Parameterized._parameters_state - self_or_cls = state.get('self', state.get('cls')) + self_, cls = state.get('self'), state.get('cls') + self_or_cls = self_ if self_ is not None else cls for k in self_or_cls._parameters_state: key = '_'+k if key in state: From de38270851aeb819e518a409b02e0dd33dea7066 Mon Sep 17 00:00:00 2001 From: maximlt Date: Mon, 3 Jul 2023 23:43:01 +0200 Subject: [PATCH 5/8] add pickle tests --- pyproject.toml | 3 + tests/testpickle.py | 171 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 tests/testpickle.py diff --git a/pyproject.toml b/pyproject.toml index 27952e360..be04f157f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ tests-full = [ "ipython", "jsonschema", "gmpy", + "cloudpickle", ] lint = [ "flake8", @@ -122,6 +123,7 @@ dependencies = [ "jsonschema", "numpy", "pandas", + "cloudpickle", # To keep __version__ up-to-date in editable installs "setuptools_scm", ] @@ -181,6 +183,7 @@ name."^(?!pypy).*".dependencies = [ "odfpy", "feather-format", "pyarrow", + "cloudpickle", ] # Only install gmpy on Linux on these version # Only install tables (deser HDF5) on Linux on these version diff --git a/tests/testpickle.py b/tests/testpickle.py new file mode 100644 index 000000000..ee7db14ba --- /dev/null +++ b/tests/testpickle.py @@ -0,0 +1,171 @@ +import pickle + +import cloudpickle +import param +import pytest + + +def eq(o1, o2): + if not sorted(o1.param) == sorted(o2.param): + return False + + for pname in o1.param: + if getattr(o1, pname) != getattr(o2, pname): + return False + return True + + +class P1(param.Parameterized): + x = param.Parameter() + +@pytest.mark.parametrize('pickler', [cloudpickle, pickle]) +def test_pickle_simple_class(pickler): + s = pickler.dumps(P1) + cls = pickler.loads(s) + assert cls is P1 + + +@pytest.mark.parametrize('pickler', [cloudpickle, pickle]) +def test_pickle_simple_instance(pickler): + p1 = P1() + s = pickler.dumps(p1) + inst = pickler.loads(s) + assert eq(p1, inst) + + +@pytest.mark.parametrize('pickler', [cloudpickle, pickle]) +def test_pickle_simple_instance_modif_after(pickler): + p1 = P1() + s = pickler.dumps(p1) + p1.x = 'modified' + inst = pickler.loads(s) + assert not eq(p1, inst) + assert inst.x is None + + +class P2(param.Parameterized): + a = param.Parameter() + b = param.String() + c = param.Dynamic() + d = param.Number() + e = param.Integer() + f = param.Action() + g = param.Event() + h = param.Callable() + i = param.Tuple() + k = param.NumericTuple() + l = param.Range() + m = param.XYCoordinates() + n = param.CalendarDateRange() + o = param.DateRange() + p = param.List() + q = param.HookList() + r = param.Path() + s = param.Filename() + t = param.Foldername() + u = param.Date() + v = param.CalendarDate() + w = param.Selector() + x = param.ObjectSelector() + y = param.FileSelector() + z = param.ListSelector() + aa = param.MultiFileSelector() + ab = param.ClassSelector(class_=type(None)) + ac = param.Series() + ad = param.Dict() + ae = param.DataFrame() + af = param.Array() + + +@pytest.mark.parametrize('pickler', [cloudpickle, pickle]) +def test_pickle_all_parameters_class(pickler): + s = pickler.dumps(P2) + cls = pickler.loads(s) + assert cls is P2 + + +@pytest.mark.parametrize('pickler', [cloudpickle, pickle]) +def test_pickle_all_parameters_instance(pickler): + p = P2() + s = pickler.dumps(p) + inst = pickler.loads(s) + assert eq(p, inst) + + +class P3(param.Parameterized): + a = param.Integer(0) + count = param.Integer(0) + + @param.depends("a", watch=True) + def cb(self): + self.count += 1 + + +@pytest.mark.parametrize('pickler', [cloudpickle, pickle]) +def test_pickle_depends_watch_class(pickler): + s = pickler.dumps(P3) + cls = pickler.loads(s) + assert cls is P3 + + +@pytest.mark.parametrize('pickler', [cloudpickle, pickle]) +def test_pickle_depends_watch_instance(pickler): + # https://github.com/holoviz/param/issues/757 + p = P3() + s = pickler.dumps(p) + inst = pickler.loads(s) + assert eq(p, inst) + + inst.a += 1 + assert inst.count == 1 + + +class P4(param.Parameterized): + a = param.Parameter() + b = param.Parameter() + + single_count = param.Integer() + attr_count = param.Integer() + single_nested_count = param.Integer() + double_nested_count = param.Integer() + nested_attr_count = param.Integer() + nested_count = param.Integer() + + @param.depends('a', watch=True) + def single_parameter(self): + self.single_count += 1 + + @param.depends('a:constant', watch=True) + def constant(self): + self.attr_count += 1 + + @param.depends('b.a', watch=True) + def single_nested(self): + self.single_nested_count += 1 + + @param.depends('b.b.a', watch=True) + def double_nested(self): + self.double_nested_count += 1 + + @param.depends('b.a:constant', watch=True) + def nested_attribute(self): + self.nested_attr_count += 1 + + @param.depends('b.param', watch=True) + def nested(self): + self.nested_count += 1 + + +@pytest.mark.parametrize('pickler', [cloudpickle, pickle]) +def test_pickle_complex_depends_class(pickler): + s = pickler.dumps(P4) + cls = pickler.loads(s) + assert cls is P4 + + +@pytest.mark.parametrize('pickler', [cloudpickle, pickle]) +def test_pickle_complex_depends_instance(pickler): + p = P4() + s = pickler.dumps(p) + inst = pickler.loads(s) + assert eq(p, inst) From f466c9cef89c4c35a4b8caed967d66e8f5a6b8f0 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 4 Jul 2023 00:07:18 +0200 Subject: [PATCH 6/8] add specific test for issue #759 --- tests/testpickle.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/testpickle.py b/tests/testpickle.py index ee7db14ba..bc4529410 100644 --- a/tests/testpickle.py +++ b/tests/testpickle.py @@ -27,19 +27,19 @@ def test_pickle_simple_class(pickler): @pytest.mark.parametrize('pickler', [cloudpickle, pickle]) def test_pickle_simple_instance(pickler): - p1 = P1() - s = pickler.dumps(p1) + p = P1() + s = pickler.dumps(p) inst = pickler.loads(s) - assert eq(p1, inst) + assert eq(p, inst) @pytest.mark.parametrize('pickler', [cloudpickle, pickle]) def test_pickle_simple_instance_modif_after(pickler): - p1 = P1() - s = pickler.dumps(p1) - p1.x = 'modified' + p = P1() + s = pickler.dumps(p) + p.x = 'modified' inst = pickler.loads(s) - assert not eq(p1, inst) + assert not eq(p, inst) assert inst.x is None @@ -169,3 +169,14 @@ def test_pickle_complex_depends_instance(pickler): s = pickler.dumps(p) inst = pickler.loads(s) assert eq(p, inst) + + +def test_issue_757(): + # https://github.com/holoviz/param/issues/759 + class P(param.Parameterized): + a = param.Parameter() + + p = P() + s = cloudpickle.dumps(p) + inst = cloudpickle.loads(s) + assert eq(p, inst) From 22bad44b5e036cde487af476f782cbcb06ce416d Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 4 Jul 2023 00:11:51 +0200 Subject: [PATCH 7/8] drop Python 3.7 --- .github/workflows/test.yaml | 2 +- pyproject.toml | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 21b89b843..2bf18c007 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: platform: ['ubuntu-latest', 'windows-latest', 'macos-latest'] - python-version: ${{ ( github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || ( github.event_name == 'push' && github.ref_type == 'tag' ) ) && fromJSON('["3.7", "3.8", "3.9", "3.10", "3.11", "pypy3.9"]') || fromJSON('["3.7", "3.9", "3.11"]') }} + python-version: ${{ ( github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || ( github.event_name == 'push' && github.ref_type == 'tag' ) ) && fromJSON('["3.8", "3.9", "3.10", "3.11", "pypy3.9"]') || fromJSON('["3.8", "3.9", "3.11"]') }} timeout-minutes: 30 steps: - uses: actions/checkout@v3 diff --git a/pyproject.toml b/pyproject.toml index 27952e360..5cd05ab88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "Make your Python code clearer and more reliable by declaring Parameters." readme = "README.md" license = { text = "BSD-3-Clause" } -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "HoloViz", email = "developers@holoviz.org" }, ] @@ -23,7 +23,6 @@ classifiers = [ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -155,7 +154,6 @@ dependencies = [ [[tool.hatch.envs.tests.matrix]] python = [ - "3.7", "3.8", "3.9", "3.10", @@ -185,8 +183,8 @@ name."^(?!pypy).*".dependencies = [ # Only install gmpy on Linux on these version # Only install tables (deser HDF5) on Linux on these version matrix.python.dependencies = [ - { value = "gmpy", if = ["3.7", "3.8", "3.9", "3.10"], platform = ["linux"] }, - { value = "tables", if = ["3.7", "3.8", "3.9", "3.10", "3.11"], platform = ["linux"] }, + { value = "gmpy", if = ["3.8", "3.9", "3.10"], platform = ["linux"] }, + { value = "tables", if = ["3.8", "3.9", "3.10", "3.11"], platform = ["linux"] }, ] [tool.hatch.envs.tests_examples] @@ -199,7 +197,6 @@ dependencies = [ [[tool.hatch.envs.tests_examples.matrix]] python = [ - "3.7", "3.8", "3.9", "3.10", From 2cba2f22c8aad1c0a89c8b86ad209e837ece7fc2 Mon Sep 17 00:00:00 2001 From: maximlt Date: Tue, 4 Jul 2023 00:15:03 +0200 Subject: [PATCH 8/8] add nest_asyncio as a dependency --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index be04f157f..0c1d16e05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ tests-full = [ "jsonschema", "gmpy", "cloudpickle", + "nest_asyncio", ] lint = [ "flake8", @@ -124,6 +125,7 @@ dependencies = [ "numpy", "pandas", "cloudpickle", + "nest_asyncio", # To keep __version__ up-to-date in editable installs "setuptools_scm", ] @@ -184,6 +186,7 @@ name."^(?!pypy).*".dependencies = [ "feather-format", "pyarrow", "cloudpickle", + "nest_asyncio", ] # Only install gmpy on Linux on these version # Only install tables (deser HDF5) on Linux on these version