Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,53 @@ See ``help(bus.request_name)`` and ``help(bus.register_object)`` for details.

.. --------------------------------------------------------------------

Error handling
==============

You can map D-Bus errors to your exception classes for better error handling.
To handle D-Bus errors, use the ``@map_error`` decorator::

from pydbus.error import map_error

@map_error("org.freedesktop.DBus.Error.InvalidArgs")
class InvalidArgsException(Exception):
pass

try:
...
catch InvalidArgsException as e:
print(e)

To register new D-Bus errors, use the ``@register_error`` decorator::

from pydbus.error import register_error

@map_error("net.lew21.pydbus.TutorialExample.MyError", MY_DOMAIN, MY_EXCEPTION_CODE)
class MyException(Exception):
pass

Then you can raise ``MyException`` from the D-Bus method of the remote object::

def Method():
raise MyException("Message")

And catch the same exception on the client side::

try:
proxy.Method()
catch MyException as e:
print(e)

To handle all unknown D-Bus errors, use the ``@map_by_default`` decorator to specify the default exception::

from pydbus.error import map_by_default

@map_by_default
class DefaultException(Exception):
pass

.. --------------------------------------------------------------------

Data types
==========

Expand Down
97 changes: 97 additions & 0 deletions pydbus/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from gi.repository import GLib, Gio


def register_error(name, domain, code):
"""Register and map decorated exception class to a DBus error."""
def decorated(cls):
error_registration.register_error(cls, name, domain, code)
return cls

return decorated


def map_error(error_name):
"""Map decorated exception class to a DBus error."""
def decorated(cls):
error_registration.map_error(cls, error_name)
return cls

return decorated


def map_by_default(cls):
"""Map decorated exception class to all unknown DBus errors."""
error_registration.map_by_default(cls)
return cls


class ErrorRegistration(object):
"""Class for mapping exceptions to DBus errors."""

_default = None
_map = dict()
_reversed_map = dict()

def map_by_default(self, exception_cls):
"""Set the exception class as a default."""
self._default = exception_cls

def map_error(self, exception_cls, name):
"""Map the exception class to a DBus name."""
self._map[name] = exception_cls
self._reversed_map[exception_cls] = name

def register_error(self, exception_cls, name, domain, code):
"""Map and register the exception class to a DBus name."""
self.map_error(exception_cls, name)
return Gio.DBusError.register_error(domain, code, name)

def is_registered_exception(self, obj):
"""Is the exception registered?"""
return obj.__class__ in self._reversed_map

def get_dbus_name(self, obj):
"""Get the DBus name of the exception."""
return self._reversed_map.get(obj.__class__)

def get_exception_class(self, name):
"""Get the exception class mapped to the DBus name."""
return self._map.get(name, self._default)

def transform_message(self, name, message):
"""Transform the message of the exception."""
prefix = "{}:{}: ".format("GDBus.Error", name)

if message.startswith(prefix):
return message[len(prefix):]

return message

def transform_exception(self, e):
"""Transform the remote error to the exception."""
if not isinstance(e, GLib.Error):
return e

if not Gio.DBusError.is_remote_error(e):
return e

# Get DBus name of the error.
name = Gio.DBusError.get_remote_error(e)
# Get the exception class.
exception_cls = self.get_exception_class(name)

# Return the original exception.
if not exception_cls:
return e

# Return new exception.
message = self.transform_message(name, e.message)
exception = exception_cls(message)
exception.dbus_name = name
exception.dbus_domain = e.domain
exception.dbus_code = e.code
return exception


# Default error registration.
error_registration = ErrorRegistration()
33 changes: 22 additions & 11 deletions pydbus/proxy_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .generic import bound_method
from .identifier import filter_identifier
from .timeout import timeout_to_glib
from .error import error_registration

try:
from inspect import Signature, Parameter
Expand Down Expand Up @@ -69,17 +70,27 @@ def __call__(self, instance, *args, **kwargs):
raise TypeError(self.__qualname__ + " got an unexpected keyword argument '{}'".format(kwarg))
timeout = kwargs.get("timeout", None)

ret = instance._bus.con.call_sync(
instance._bus_name, instance._path,
self._iface_name, self.__name__, GLib.Variant(self._sinargs, args), GLib.VariantType.new(self._soutargs),
0, timeout_to_glib(timeout), None).unpack()

if len(self._outargs) == 0:
return None
elif len(self._outargs) == 1:
return ret[0]
else:
return ret
error = None
result = None
try:
ret = instance._bus.con.call_sync(
instance._bus_name, instance._path,
self._iface_name, self.__name__, GLib.Variant(self._sinargs, args), GLib.VariantType.new(self._soutargs),
0, timeout_to_glib(timeout), None).unpack()

if len(self._outargs) == 0:
result = None
elif len(self._outargs) == 1:
result = ret[0]
else:
result = ret
except Exception as e:
error = error_registration.transform_exception(e)

if error:
raise error

return result

def __get__(self, instance, owner):
if instance is None:
Expand Down
16 changes: 11 additions & 5 deletions pydbus/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .exitable import ExitableWithAliases
from functools import partial
from .method_call_context import MethodCallContext
from .error import error_registration
import logging

try:
Expand Down Expand Up @@ -91,11 +92,16 @@ def call_method(self, connection, sender, object_path, interface_name, method_na
logger = logging.getLogger(__name__)
logger.exception("Exception while handling %s.%s()", interface_name, method_name)

#TODO Think of a better way to translate Python exception types to DBus error types.
e_type = type(e).__name__
if not "." in e_type:
e_type = "unknown." + e_type
invocation.return_dbus_error(e_type, str(e))
if error_registration.is_registered_exception(e):
name = error_registration.get_dbus_name(e)
invocation.return_dbus_error(name, str(e))
else:
logger.info("name is not registered")
e_type = type(e).__name__
if not "." in e_type:
e_type = "unknown." + e_type

invocation.return_dbus_error(e_type, str(e))

def Get(self, interface_name, property_name):
type = self.readable_properties[interface_name + "." + property_name]
Expand Down
67 changes: 67 additions & 0 deletions tests/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from pydbus.error import ErrorRegistration


class ExceptionA(Exception):
pass


class ExceptionB(Exception):
pass


class ExceptionC(Exception):
pass


class ExceptionD(Exception):
pass


class ExceptionE(Exception):
pass


def test_error_mapping():
r = ErrorRegistration()
r.map_error(ExceptionA, "net.lew21.pydbus.tests.ErrorA")
r.map_error(ExceptionB, "net.lew21.pydbus.tests.ErrorB")
r.map_error(ExceptionC, "net.lew21.pydbus.tests.ErrorC")

assert r.is_registered_exception(ExceptionA("Test"))
assert r.is_registered_exception(ExceptionB("Test"))
assert r.is_registered_exception(ExceptionC("Test"))
assert not r.is_registered_exception(ExceptionD("Test"))
assert not r.is_registered_exception(ExceptionE("Test"))

assert r.get_dbus_name(ExceptionA("Test")) == "net.lew21.pydbus.tests.ErrorA"
assert r.get_dbus_name(ExceptionB("Test")) == "net.lew21.pydbus.tests.ErrorB"
assert r.get_dbus_name(ExceptionC("Test")) == "net.lew21.pydbus.tests.ErrorC"

assert r.get_exception_class("net.lew21.pydbus.tests.ErrorA") == ExceptionA
assert r.get_exception_class("net.lew21.pydbus.tests.ErrorB") == ExceptionB
assert r.get_exception_class("net.lew21.pydbus.tests.ErrorC") == ExceptionC
assert r.get_exception_class("net.lew21.pydbus.tests.ErrorD") is None
assert r.get_exception_class("net.lew21.pydbus.tests.ErrorE") is None

r.map_by_default(ExceptionD)
assert not r.is_registered_exception(ExceptionD("Test"))
assert r.get_exception_class("net.lew21.pydbus.tests.ErrorD") == ExceptionD
assert r.get_exception_class("net.lew21.pydbus.tests.ErrorE") == ExceptionD


def test_transform_message():
r = ErrorRegistration()
n1 = "net.lew21.pydbus.tests.ErrorA"
m1 = "GDBus.Error:net.lew21.pydbus.tests.ErrorA: Message1"

n2 = "net.lew21.pydbus.tests.ErrorB"
m2 = "GDBus.Error:net.lew21.pydbus.tests.ErrorB: Message2"

assert r.transform_message(n1, m1) == "Message1"
assert r.transform_message(n2, m2) == "Message2"
assert r.transform_message(n1, m2) == m2
assert r.transform_message(n2, m1) == m1


test_error_mapping()
test_transform_message()
Loading