diff --git a/param/_async.py b/param/_async.py new file mode 100644 index 000000000..144c50782 --- /dev/null +++ b/param/_async.py @@ -0,0 +1,29 @@ +""" +Module that implements asyncio.coroutine function wrappers to be used +by param internal callbacks. These are defined in a separate file due +to py2 incompatibility with both `async/await` and `yield from` syntax. +""" + +import asyncio + +def generate_depends(func): + @asyncio.coroutine + def _depends(*args, **kw): + yield from func(*args, **kw) # noqa: E999 + return _depends + +def generate_caller(function, what='value', changed=None, callback=None, skip_event=None): + @asyncio.coroutine + def caller(*events): + if callback: callback(*events) + if not skip_event or not skip_event(*events, what=what, changed=changed): + yield from function() # noqa: E999 + return caller + +def generate_callback(func, dependencies, kw): + @asyncio.coroutine + def cb(*events): + args = (getattr(dep.owner, dep.name) for dep in dependencies) + dep_kwargs = {n: getattr(dep.owner, dep.name) for n, dep in kw.items()} + yield from func(*args, **dep_kwargs) # noqa: E999 + return cb diff --git a/param/parameterized.py b/param/parameterized.py index fff302156..86f88281a 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -390,10 +390,8 @@ def depends(func, *dependencies, **kw): on_init = kw.pop("on_init", False) if iscoroutinefunction(func): - import asyncio - @asyncio.coroutine - def _depends(*args, **kw): - yield from func(*args, **kw) + from ._async import generate_depends + _depends = generate_depends(func) else: @wraps(func) def _depends(*args, **kw): @@ -430,12 +428,8 @@ def _depends(*args, **kw): if not string_specs and watch: # string_specs case handled elsewhere (later), in Parameterized.__init__ if iscoroutinefunction(func): - import asyncio - @asyncio.coroutine - def cb(*events): - args = (getattr(dep.owner, dep.name) for dep in dependencies) - dep_kwargs = {n: getattr(dep.owner, dep.name) for n, dep in kw.items()} - yield from func(*args, **dep_kwargs) + from ._async import generate_callback + cb = generate_callback(func, dependencies, kw) else: def cb(*events): args = (getattr(dep.owner, dep.name) for dep in dependencies) @@ -657,12 +651,8 @@ def _m_caller(self, method_name, what='value', changed=None, callback=None): """ function = getattr(self, method_name) if iscoroutinefunction(function): - import asyncio - @asyncio.coroutine - def caller(*events): - if callback: callback(*events) - if not _skip_event(*events, what=what, changed=changed): - yield from function() + from ._async import generate_caller + caller = generate_caller(function, what=what, changed=changed, callback=callback, skip_event=_skip_event) else: def caller(*events): if callback: callback(*events) diff --git a/tests/API1/testparamdepends.py b/tests/API1/testparamdepends.py index 3128c6b9d..2c5e586ef 100644 --- a/tests/API1/testparamdepends.py +++ b/tests/API1/testparamdepends.py @@ -1,15 +1,30 @@ """ Unit test for param.depends. """ - -import pytest +import sys import param +import pytest from param.parameterized import _parse_dependency_spec from . import API1TestCase +try: + import asyncio +except ImportError: + asyncio = None + + +def async_executor(func): + # Could be entirely replaced by asyncio.run(func()) in Python >=3.7 + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(func()) + class TestDependencyParser(API1TestCase): @@ -638,7 +653,25 @@ def test_param_external_param_instance(self): self.assertIs(pinfo.inst, None) self.assertEqual(pinfo.name, 'a') self.assertEqual(pinfo.what, 'value') - + + @pytest.mark.skipif(sys.version_info.major == 2, reason='asyncio only on Python 3') + 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) + @asyncio.coroutine + 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 TestParamDependsFunction(API1TestCase): @@ -711,6 +744,25 @@ def function(value, c): p.b = 3 self.assertEqual(d, [4, 5]) + @pytest.mark.skipif(sys.version_info.major == 2, reason='asyncio only on Python 3') + def test_async(self): + try: + param.parameterized.async_executor = async_executor + p = self.P(a=1) + + d = [] + + @param.depends(p.param.a, watch=True) + @asyncio.coroutine + def function(value): + d.append(value) + + p.a = 2 + + assert d == [2] + finally: + param.parameterized.async_executor = None + def test_misspelled_parameter_in_depends(): class Example(param.Parameterized):