From b34051ccafb1d4649440258282633523fd240357 Mon Sep 17 00:00:00 2001 From: Ryan Young Date: Wed, 13 Jul 2022 15:46:47 -0700 Subject: [PATCH] Fix notify() failing when being called from an existing asynchronous event loop (#624) --- apprise/Apprise.py | 6 ++-- apprise/py3compat/asyncio.py | 25 ++++++++++++++-- test/test_asyncio.py | 55 ++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/apprise/Apprise.py b/apprise/Apprise.py index 30c93653..66395a3e 100644 --- a/apprise/Apprise.py +++ b/apprise/Apprise.py @@ -424,7 +424,7 @@ class Apprise(object): except TypeError: # No notifications sent, and there was an internal error. - return py3compat.asyncio.toasyncwrap(False) + return py3compat.asyncio.toasyncwrapvalue(False) else: if len(coroutines) > 0: @@ -433,7 +433,7 @@ class Apprise(object): else: # No notifications sent. - return py3compat.asyncio.toasyncwrap(None) + return py3compat.asyncio.toasyncwrapvalue(None) @staticmethod def _notifyhandler(server, **kwargs): @@ -470,7 +470,7 @@ class Apprise(object): # Send the notification immediately, and wrap the result in a # coroutine. status = Apprise._notifyhandler(server, **kwargs) - return py3compat.asyncio.toasyncwrap(status) + return py3compat.asyncio.toasyncwrapvalue(status) def _notifyall(self, handler, body, title='', notify_type=NotifyType.INFO, body_format=None, tag=MATCH_ALL_TAG, match_always=True, diff --git a/apprise/py3compat/asyncio.py b/apprise/py3compat/asyncio.py index 3724c8a0..ddcad010 100644 --- a/apprise/py3compat/asyncio.py +++ b/apprise/py3compat/asyncio.py @@ -63,7 +63,20 @@ def tosync(cor, debug=False): """ if ASYNCIO_RUN_SUPPORT: - return asyncio.run(cor, debug=debug) + try: + loop = asyncio.get_running_loop() + + except RuntimeError: + # There is no existing event loop, so we can start our own. + return asyncio.run(cor, debug=debug) + + else: + # Enable debug mode + loop.set_debug(debug) + + # Run the coroutine and wait for the result. + task = loop.create_task(cor) + return asyncio.ensure_future(task, loop=loop) else: # The Deprecated Way (<= Python v3.6) @@ -85,7 +98,7 @@ def tosync(cor, debug=False): return loop.run_until_complete(cor) -async def toasyncwrap(v): # noqa: E999 +async def toasyncwrapvalue(v): # noqa: E999 """ Create a coroutine that, when run, returns the provided value. """ @@ -93,6 +106,14 @@ async def toasyncwrap(v): # noqa: E999 return v +async def toasyncwrap(fn): # noqa: E999 + """ + Create a coroutine that, when run, executes the provided function. + """ + + return fn() + + class AsyncNotifyBase(URLBase): """ asyncio wrapper for the NotifyBase object diff --git a/test/test_asyncio.py b/test/test_asyncio.py index 68a26b1e..088c8fb4 100644 --- a/test/test_asyncio.py +++ b/test/test_asyncio.py @@ -24,6 +24,7 @@ # THE SOFTWARE. from __future__ import print_function +import six import sys import pytest from apprise import Apprise @@ -32,6 +33,9 @@ from apprise import NotifyFormat from apprise.plugins import SCHEMA_MAP +if not six.PY2: + import apprise.py3compat.asyncio as py3aio + # Disable logging for a cleaner testing output import logging logging.disable(logging.CRITICAL) @@ -106,3 +110,54 @@ def test_apprise_asyncio_runtime_error(): finally: # Restore our event loop (in the event the above test failed) asyncio.set_event_loop(loop) + + +@pytest.mark.skipif(sys.version_info.major <= 2 or sys.version_info < (3, 7), + reason="Requires Python 3.7+") +def test_apprise_works_in_async_loop(): + """ + API: Apprise() can execute synchronously in an existing event loop + + """ + class GoodNotification(NotifyBase): + def __init__(self, **kwargs): + super(GoodNotification, self).__init__( + notify_format=NotifyFormat.HTML, **kwargs) + + def url(self, **kwargs): + # Support URL + return '' + + def send(self, **kwargs): + # Pretend everything is okay + return True + + @staticmethod + def parse_url(url, *args, **kwargs): + # always parseable + return NotifyBase.parse_url(url, verify_host=False) + + # Store our good notification in our schema map + SCHEMA_MAP['good'] = GoodNotification + + # Create ourselves an Apprise object + a = Apprise() + + # Add a few entries + for _ in range(25): + a.add('good://') + + # To ensure backwards compatibility, it should be possible to call + # asynchronous Apprise methods from code that already uses an event loop, + # even when using the synchronous notify() method. + # see https://github.com/caronc/apprise/issues/610 + import asyncio + + def try_notify(): + a.notify(title="title", body="body") + + # Convert to a coroutine to run asynchronously. + cor = py3aio.toasyncwrap(try_notify) + + # Should execute successfully. + asyncio.run(cor)