Skip to content

Commit

Permalink
Notifications: fixes, cleanup, and tests (#131)
Browse files Browse the repository at this point in the history
* notifications: fixes, cleanup, and tests

* add notification tests for _init_nsapp and _default_user_notification_center

* add to Notification class with pythonic interpretation of objc notification object

* log changes for notifications
  • Loading branch information
jaredks committed May 9, 2020
1 parent e2cdc6e commit 7fb9e87
Show file tree
Hide file tree
Showing 11 changed files with 553 additions and 210 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Expand Up @@ -4,6 +4,7 @@ Changes
0.3.0.dev
---------

- Notifications: fixes, cleanup, and tests #131
- Fix slider for some older macOS versions (10.11 and before?)
- Keyboard interrupts now stop a running application

Expand Down
8 changes: 6 additions & 2 deletions rumps/__init__.py
Expand Up @@ -22,5 +22,9 @@
__license__ = 'Modified BSD'
__copyright__ = 'Copyright 2020 Jared Suttles'

from .rumps import (separator, debug_mode, alert, notification, application_support, timers, quit_application, timer,
clicked, notifications, MenuItem, SliderMenuItem, Timer, Window, App, slider)
from . import notifications as _notifications
from .rumps import (separator, debug_mode, alert, application_support, timers, quit_application, timer,
clicked, MenuItem, SliderMenuItem, Timer, Window, App, slider)

notifications = _notifications.on_notification
notification = _notifications.notify
92 changes: 92 additions & 0 deletions rumps/_internal.py
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-

from __future__ import print_function

import inspect
import traceback

import Foundation

from . import compat
from . import exceptions


def require_string(*objs):
for obj in objs:
if not isinstance(obj, compat.string_types):
raise TypeError(
'a string is required but given {0}, a {1}'.format(obj, type(obj).__name__)
)


def require_string_or_none(*objs):
for obj in objs:
if not(obj is None or isinstance(obj, compat.string_types)):
raise TypeError(
'a string or None is required but given {0}, a {1}'.format(obj, type(obj).__name__)
)


def call_as_function_or_method(func, event):
# The idea here is that when using decorators in a class, the functions passed are not bound so we have to
# determine later if the functions we have (those saved as callbacks) for particular events need to be passed
# 'self'.
#
# This works for an App subclass method or a standalone decorated function. Will attempt to find function as
# a bound method of the App instance. If it is found, use it, otherwise simply call function.
from . import rumps
try:
app = getattr(rumps.App, '*app_instance')
except AttributeError:
pass
else:
for name, method in inspect.getmembers(app, predicate=inspect.ismethod):
if method.__func__ is func:
return method(event)
return func(event)


def guard_unexpected_errors(func):
"""Decorator to be used in PyObjC callbacks where an error bubbling up
would cause a crash. Instead of crashing, print the error to stderr and
prevent passing to PyObjC layer.
For Python 3, print the exception using chaining. Accomplished by setting
the cause of :exc:`rumps.exceptions.InternalRumpsError` to the exception.
For Python 2, emulate exception chaining by printing the original exception
followed by :exc:`rumps.exceptions.InternalRumpsError`.
"""
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)

except Exception as e:
internal_error = exceptions.InternalRumpsError(
'an unexpected error occurred within an internal callback'
)
if compat.PY2:
import sys
traceback.print_exc()
print('\nThe above exception was the direct cause of the following exception:\n', file=sys.stderr)
traceback.print_exception(exceptions.InternalRumpsError, internal_error, None)
else:
internal_error.__cause__ = e
traceback.print_exception(exceptions.InternalRumpsError, internal_error, None)

return wrapper


def string_to_objc(x):
if isinstance(x, compat.binary_type):
return Foundation.NSData.alloc().initWithData_(x)
elif isinstance(x, compat.string_types):
return Foundation.NSString.alloc().initWithString_(x)
else:
raise TypeError(
"expected a string or a bytes-like object but provided %s, "
"having type '%s'" % (
x,
type(x).__name__
)
)
6 changes: 6 additions & 0 deletions rumps/compat.py
Expand Up @@ -15,13 +15,19 @@
PY2 = sys.version_info[0] == 2

if not PY2:
binary_type = bytes
text_type = str
string_types = (str,)

iteritems = lambda d: iter(d.items())

import collections.abc as collections_abc

else:
binary_type = ()
text_type = unicode
string_types = (str, unicode)

iteritems = lambda d: d.iteritems()

import collections as collections_abc
9 changes: 9 additions & 0 deletions rumps/exceptions.py
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-


class RumpsError(Exception):
"""A generic rumps error occurred."""


class InternalRumpsError(RumpsError):
"""Internal mechanism powering functionality of rumps failed."""

0 comments on commit 7fb9e87

Please sign in to comment.