Skip to content

Commit

Permalink
refactored asyncio into py3compat apprise module
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc committed Aug 15, 2020
1 parent 4f3aa3e commit 56e0c6e
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 114 deletions.
70 changes: 11 additions & 59 deletions apprise/Apprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,10 @@
from . import plugins
from . import __version__

try:
# asyncio wrapper for Python 3
import asyncio
ASYNCIO_SUPPORT = True

except SyntaxError:
# Python v2.7
ASYNCIO_SUPPORT = False
# Python v3+ support code made importable so it can remain backwards
# compatible with Python v2
from . import py3compat
ASYNCIO_SUPPORT = not six.PY2


class Apprise(object):
Expand Down Expand Up @@ -337,7 +333,7 @@ def notify(self, body, title='', notify_type=NotifyType.INFO,

# for asyncio support; we track a list of our servers to notify
# sequentially
async_servers = []
coroutines = []

# Iterate over our loaded plugins
for server in self.find(tag):
Expand Down Expand Up @@ -399,7 +395,7 @@ def notify(self, body, title='', notify_type=NotifyType.INFO,
if ASYNCIO_SUPPORT and server.asset.async_mode:
# Build a list of servers requiring notification
# that will be triggered asynchronously afterwards
async_servers.append(server.async_notify(
coroutines.append(server.async_notify(
body=conversion_map[server.notify_format],
title=title,
notify_type=notify_type,
Expand Down Expand Up @@ -429,55 +425,11 @@ def notify(self, body, title='', notify_type=NotifyType.INFO,
logger.exception("Notification Exception")
status = False

if ASYNCIO_SUPPORT and async_servers:
async def main(results, *async_servers):
"""
Task: Notify all servers specified and return our result set
in a mutable object.
"""
try:
results['response'] = await asyncio.gather(*async_servers)

except AttributeError:
# AttributeError: module 'asyncio' has no attribute
# 'gather'
# Error is thrown for Python < v3.7
results['response'] = await asyncio.wait(async_servers)

# Create a mutable object we can get our results from
results = {}

# Notify all of our servers
# export PYTHONASYNCIODEBUG=1 to enable debugging mode
logger.info('Notifying {} services asynchronous.'
.format(len(async_servers)))

try:

# send our notifications
asyncio.run(main(results, *async_servers))

except AttributeError:
# AttributeError: module 'asyncio' has no attribute 'run'
# Error is thrown for Python < v3.7
loop = asyncio.get_event_loop()

loop.run_until_complete(
asyncio.wait(main(results, *async_servers)))

loop.close()

# The below iterates over all of our responses and keys in
# on False returns. Then it considers that we may only be
# running asynchronous for some of the notification services
# (while others have already returned there value).
#
# The below considers that one failed service outside of
# the asynchronous notification trumps a True state where
# all other services were successful
status = next(
(s for s in results['response'] if s is False),
status if status is False else True)
if coroutines:
# perform our async notification(s)
if not py3compat.asyncio.notify(coroutines):
# Toggle our status only if we had a failure
status = False

return status

Expand Down
51 changes: 0 additions & 51 deletions apprise/plugins/AsyncNotifyBase.py

This file was deleted.

2 changes: 1 addition & 1 deletion apprise/plugins/NotifyBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

if six.PY3:
# Wrap our base with the asyncio wrapper
from .AsyncNotifyBase import AsyncNotifyBase
from ..py3compat.asyncio import AsyncNotifyBase
BASE_OBJECT = AsyncNotifyBase

else:
Expand Down
Empty file added apprise/py3compat/__init__.py
Empty file.
115 changes: 115 additions & 0 deletions apprise/py3compat/asyncio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-

# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# 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.

import sys
import asyncio
from ..URLBase import URLBase
from ..logger import logger


# A global flag that tracks if we are Python v3.7 or higher
ASYNCIO_RUN_SUPPORT = \
sys.version_info.major > 3 or \
(sys.version_info.major == 3 and sys.version_info.minor >= 7)


def notify(coroutines, debug=False):
"""
A Wrapper to the AsyncNotifyBase.async_notify() calls allowing us
to call gather() and collect the responses
"""

# Create log entry
logger.info(
'Notifying {} service(s) asynchronous.'.format(len(coroutines)))

if ASYNCIO_RUN_SUPPORT:
# async reference produces a SyntaxError (E999) in Python v2.7
# For this reason we turn on the noqa flag
async def main(results, coroutines): # noqa: E999
"""
Task: Notify all servers specified and return our result set
through a mutable object.
"""
# send our notifications and store our result set into
# our results dictionary
results['response'] = \
await asyncio.gather(*coroutines, return_exceptions=True)

# Initialize a mutable object we can populate with our notification
# responses
results = {}

# Send our notifications
asyncio.run(main(results, coroutines), debug=debug)

# Acquire our return status
status = next((s for s in results['response'] if s is False), True)

else:
#
# The depricated way
#

# acquire access to our event loop
loop = asyncio.get_event_loop()

if debug:
# Enable debug mode
loop.set_debug(1)

# Send our notifications and acquire our status
results = loop.run_until_complete(asyncio.gather(*coroutines))

# Acquire our return status
status = next((r for r in results if r is False), True)

# Returns True if all notifications succeeded, otherwise False is
# returned.
return status


class AsyncNotifyBase(URLBase):
"""
asyncio wrapper for the NotifyBase object
"""

async def async_notify(self, *args, **kwargs): # noqa: E999
"""
Async Notification Wrapper
"""
try:
return self.notify(*args, **kwargs)

except TypeError:
# These our our internally thrown notifications
pass

except Exception:
# A catch all so we don't have to abort early
# just because one of our plugins has a bug in it.
logger.exception("Notification Exception")

return False
6 changes: 3 additions & 3 deletions test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
logging.disable(logging.CRITICAL)


def test_apprise_cli(tmpdir):
def test_apprise_cli_nux_env(tmpdir):
"""
API: Apprise() CLI
CLI: Nux Environment
"""

Expand Down Expand Up @@ -302,7 +302,7 @@ def url(self, *args, **kwargs):
@mock.patch('platform.system')
def test_apprise_cli_windows_env(mock_system):
"""
API: Apprise() CLI Windows Environment
CLI: Windows Environment
"""
# Force a windows environment
Expand Down

0 comments on commit 56e0c6e

Please sign in to comment.