From 221e304a73c51de898dc1cb443572c22c6d519af Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Mon, 22 Feb 2021 14:16:35 -0500 Subject: [PATCH] Better asyncio handling in Python <3.7 using threading (#364) --- apprise/py3compat/asyncio.py | 15 ++++- test/test_asyncio.py | 108 +++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 test/test_asyncio.py diff --git a/apprise/py3compat/asyncio.py b/apprise/py3compat/asyncio.py index a2dd68af..c5aae888 100644 --- a/apprise/py3compat/asyncio.py +++ b/apprise/py3compat/asyncio.py @@ -72,11 +72,20 @@ def notify(coroutines, debug=False): else: # - # The depricated way + # The Deprecated Way (<= Python v3.6) # - # acquire access to our event loop - loop = asyncio.get_event_loop() + try: + # acquire access to our event loop + loop = asyncio.get_event_loop() + + except RuntimeError: + # This happens if we're inside a thread of another application + # where there is no running event_loop(). Pythong v3.7 and higher + # automatically take care of this case for us. But for the lower + # versions we need to do the following: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) if debug: # Enable debug mode diff --git a/test/test_asyncio.py b/test/test_asyncio.py new file mode 100644 index 00000000..67daa132 --- /dev/null +++ b/test/test_asyncio.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Chris Caron +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function +import sys +import pytest +from apprise import Apprise +from apprise import NotifyBase +from apprise import NotifyFormat + +from apprise.plugins import SCHEMA_MAP + +# Disable logging for a cleaner testing output +import logging +logging.disable(logging.CRITICAL) + + +@pytest.mark.skipif(sys.version_info.major <= 2 or sys.version_info >= (3, 7), + reason="Requires Python 3.0 to 3.6") +def test_apprise_asyncio_runtime_error(): + """ + API: Apprise() AsyncIO RuntimeError handling + + """ + class GoodNotification(NotifyBase): + def __init__(self, **kwargs): + super(GoodNotification, self).__init__( + notify_format=NotifyFormat.HTML, **kwargs) + + def url(self): + # 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://') + + # Python v3.6 and lower can't handle situations gracefully when an + # event_loop isn't already established(). Test that Apprise can handle + # these situations + import asyncio + + # Get our event loop + loop = asyncio.get_event_loop() + + # Adjust out event loop to not point at anything + asyncio.set_event_loop(None) + + # With the event loop inactive, we'll fail trying to get the active loop + with pytest.raises(RuntimeError): + asyncio.get_event_loop() + + try: + # Below, we internally will throw a RuntimeError() since there will + # be no active event_loop in place. However internally it will be smart + # enough to create a new event loop and continue... + assert a.notify(title="title", body="body") is True + + # Verify we have an active event loop + new_loop = asyncio.get_event_loop() + + # We didn't throw an exception above; thus we have an event loop at + # this point + assert new_loop + + # Close off the internal loop created inside a.notify() + new_loop.close() + + finally: + # Restore our event loop (in the event the above test failed) + asyncio.set_event_loop(loop)