summaryrefslogtreecommitdiffstats
path: root/meta-python/recipes-devtools/python/python3-pydbus/0003-Support-transformation-between-D-Bus-errors-and-exce.patch
diff options
context:
space:
mode:
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-pydbus/0003-Support-transformation-between-D-Bus-errors-and-exce.patch')
-rw-r--r--meta-python/recipes-devtools/python/python3-pydbus/0003-Support-transformation-between-D-Bus-errors-and-exce.patch495
1 files changed, 495 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-pydbus/0003-Support-transformation-between-D-Bus-errors-and-exce.patch b/meta-python/recipes-devtools/python/python3-pydbus/0003-Support-transformation-between-D-Bus-errors-and-exce.patch
new file mode 100644
index 0000000000..a1b8a6c38c
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-pydbus/0003-Support-transformation-between-D-Bus-errors-and-exce.patch
@@ -0,0 +1,495 @@
1From 773858e1afd21cdf3ceef2cd35509f0b4882bf16 Mon Sep 17 00:00:00 2001
2From: Vendula Poncova <vponcova@redhat.com>
3Date: Tue, 1 Aug 2017 16:54:24 +0200
4Subject: [PATCH 3/3] Support transformation between D-Bus errors and
5 exceptions.
6
7Exceptions can be registered with decorators, raised in a remote
8method and recreated after return from the remote call.
9
10Adapted from Fedora [https://src.fedoraproject.org/cgit/rpms/python-pydbus.git/]
11
12Upstream-Status: Inactive-Upstream (Last release 12/18/2016; Last commit 05/6/2018)
13
14Signed-off-by: Derek Straka <derek@asterius.io>
15---
16 doc/tutorial.rst | 47 ++++++++++++++++++
17 pydbus/error.py | 97 ++++++++++++++++++++++++++++++++++++
18 pydbus/proxy_method.py | 18 +++++--
19 pydbus/registration.py | 16 ++++--
20 tests/error.py | 67 +++++++++++++++++++++++++
21 tests/publish_error.py | 132 +++++++++++++++++++++++++++++++++++++++++++++++++
22 tests/run.sh | 2 +
23 7 files changed, 371 insertions(+), 8 deletions(-)
24 create mode 100644 pydbus/error.py
25 create mode 100644 tests/error.py
26 create mode 100644 tests/publish_error.py
27
28diff --git a/doc/tutorial.rst b/doc/tutorial.rst
29index b8479cf..7fe55e1 100644
30--- a/doc/tutorial.rst
31+++ b/doc/tutorial.rst
32@@ -341,6 +341,53 @@ See ``help(bus.request_name)`` and ``help(bus.register_object)`` for details.
33
34 .. --------------------------------------------------------------------
35
36+Error handling
37+==============
38+
39+You can map D-Bus errors to your exception classes for better error handling.
40+To handle D-Bus errors, use the ``@map_error`` decorator::
41+
42+ from pydbus.error import map_error
43+
44+ @map_error("org.freedesktop.DBus.Error.InvalidArgs")
45+ class InvalidArgsException(Exception):
46+ pass
47+
48+ try:
49+ ...
50+ catch InvalidArgsException as e:
51+ print(e)
52+
53+To register new D-Bus errors, use the ``@register_error`` decorator::
54+
55+ from pydbus.error import register_error
56+
57+ @map_error("net.lew21.pydbus.TutorialExample.MyError", MY_DOMAIN, MY_EXCEPTION_CODE)
58+ class MyException(Exception):
59+ pass
60+
61+Then you can raise ``MyException`` from the D-Bus method of the remote object::
62+
63+ def Method():
64+ raise MyException("Message")
65+
66+And catch the same exception on the client side::
67+
68+ try:
69+ proxy.Method()
70+ catch MyException as e:
71+ print(e)
72+
73+To handle all unknown D-Bus errors, use the ``@map_by_default`` decorator to specify the default exception::
74+
75+ from pydbus.error import map_by_default
76+
77+ @map_by_default
78+ class DefaultException(Exception):
79+ pass
80+
81+.. --------------------------------------------------------------------
82+
83 Data types
84 ==========
85
86diff --git a/pydbus/error.py b/pydbus/error.py
87new file mode 100644
88index 0000000..aaa3510
89--- /dev/null
90+++ b/pydbus/error.py
91@@ -0,0 +1,97 @@
92+from gi.repository import GLib, Gio
93+
94+
95+def register_error(name, domain, code):
96+ """Register and map decorated exception class to a DBus error."""
97+ def decorated(cls):
98+ error_registration.register_error(cls, name, domain, code)
99+ return cls
100+
101+ return decorated
102+
103+
104+def map_error(error_name):
105+ """Map decorated exception class to a DBus error."""
106+ def decorated(cls):
107+ error_registration.map_error(cls, error_name)
108+ return cls
109+
110+ return decorated
111+
112+
113+def map_by_default(cls):
114+ """Map decorated exception class to all unknown DBus errors."""
115+ error_registration.map_by_default(cls)
116+ return cls
117+
118+
119+class ErrorRegistration(object):
120+ """Class for mapping exceptions to DBus errors."""
121+
122+ _default = None
123+ _map = dict()
124+ _reversed_map = dict()
125+
126+ def map_by_default(self, exception_cls):
127+ """Set the exception class as a default."""
128+ self._default = exception_cls
129+
130+ def map_error(self, exception_cls, name):
131+ """Map the exception class to a DBus name."""
132+ self._map[name] = exception_cls
133+ self._reversed_map[exception_cls] = name
134+
135+ def register_error(self, exception_cls, name, domain, code):
136+ """Map and register the exception class to a DBus name."""
137+ self.map_error(exception_cls, name)
138+ return Gio.DBusError.register_error(domain, code, name)
139+
140+ def is_registered_exception(self, obj):
141+ """Is the exception registered?"""
142+ return obj.__class__ in self._reversed_map
143+
144+ def get_dbus_name(self, obj):
145+ """Get the DBus name of the exception."""
146+ return self._reversed_map.get(obj.__class__)
147+
148+ def get_exception_class(self, name):
149+ """Get the exception class mapped to the DBus name."""
150+ return self._map.get(name, self._default)
151+
152+ def transform_message(self, name, message):
153+ """Transform the message of the exception."""
154+ prefix = "{}:{}: ".format("GDBus.Error", name)
155+
156+ if message.startswith(prefix):
157+ return message[len(prefix):]
158+
159+ return message
160+
161+ def transform_exception(self, e):
162+ """Transform the remote error to the exception."""
163+ if not isinstance(e, GLib.Error):
164+ return e
165+
166+ if not Gio.DBusError.is_remote_error(e):
167+ return e
168+
169+ # Get DBus name of the error.
170+ name = Gio.DBusError.get_remote_error(e)
171+ # Get the exception class.
172+ exception_cls = self.get_exception_class(name)
173+
174+ # Return the original exception.
175+ if not exception_cls:
176+ return e
177+
178+ # Return new exception.
179+ message = self.transform_message(name, e.message)
180+ exception = exception_cls(message)
181+ exception.dbus_name = name
182+ exception.dbus_domain = e.domain
183+ exception.dbus_code = e.code
184+ return exception
185+
186+
187+# Default error registration.
188+error_registration = ErrorRegistration()
189diff --git a/pydbus/proxy_method.py b/pydbus/proxy_method.py
190index 442fe07..a73f9eb 100644
191--- a/pydbus/proxy_method.py
192+++ b/pydbus/proxy_method.py
193@@ -2,6 +2,7 @@ from gi.repository import GLib
194 from .generic import bound_method
195 from .identifier import filter_identifier
196 from .timeout import timeout_to_glib
197+from .error import error_registration
198
199 try:
200 from inspect import Signature, Parameter
201@@ -87,9 +88,20 @@ class ProxyMethod(object):
202 call_args += (self._finish_async_call, (callback, callback_args))
203 instance._bus.con.call(*call_args)
204 return None
205+
206 else:
207- ret = instance._bus.con.call_sync(*call_args)
208- return self._unpack_return(ret)
209+ result = None
210+ error = None
211+
212+ try:
213+ result = instance._bus.con.call_sync(*call_args)
214+ except Exception as e:
215+ error = error_registration.transform_exception(e)
216+
217+ if error:
218+ raise error
219+
220+ return self._unpack_return(result)
221
222 def _unpack_return(self, values):
223 ret = values.unpack()
224@@ -108,7 +120,7 @@ class ProxyMethod(object):
225 ret = source.call_finish(result)
226 return_args = self._unpack_return(ret)
227 except Exception as err:
228- error = err
229+ error = error_registration.transform_exception(err)
230
231 callback, callback_args = user_data
232 callback(*callback_args, returned=return_args, error=error)
233diff --git a/pydbus/registration.py b/pydbus/registration.py
234index f531539..1d2cbcb 100644
235--- a/pydbus/registration.py
236+++ b/pydbus/registration.py
237@@ -5,6 +5,7 @@ from . import generic
238 from .exitable import ExitableWithAliases
239 from functools import partial
240 from .method_call_context import MethodCallContext
241+from .error import error_registration
242 import logging
243
244 try:
245@@ -91,11 +92,16 @@ class ObjectWrapper(ExitableWithAliases("unwrap")):
246 logger = logging.getLogger(__name__)
247 logger.exception("Exception while handling %s.%s()", interface_name, method_name)
248
249- #TODO Think of a better way to translate Python exception types to DBus error types.
250- e_type = type(e).__name__
251- if not "." in e_type:
252- e_type = "unknown." + e_type
253- invocation.return_dbus_error(e_type, str(e))
254+ if error_registration.is_registered_exception(e):
255+ name = error_registration.get_dbus_name(e)
256+ invocation.return_dbus_error(name, str(e))
257+ else:
258+ logger.info("name is not registered")
259+ e_type = type(e).__name__
260+ if not "." in e_type:
261+ e_type = "unknown." + e_type
262+
263+ invocation.return_dbus_error(e_type, str(e))
264
265 def Get(self, interface_name, property_name):
266 type = self.readable_properties[interface_name + "." + property_name]
267diff --git a/tests/error.py b/tests/error.py
268new file mode 100644
269index 0000000..3ec507d
270--- /dev/null
271+++ b/tests/error.py
272@@ -0,0 +1,67 @@
273+from pydbus.error import ErrorRegistration
274+
275+
276+class ExceptionA(Exception):
277+ pass
278+
279+
280+class ExceptionB(Exception):
281+ pass
282+
283+
284+class ExceptionC(Exception):
285+ pass
286+
287+
288+class ExceptionD(Exception):
289+ pass
290+
291+
292+class ExceptionE(Exception):
293+ pass
294+
295+
296+def test_error_mapping():
297+ r = ErrorRegistration()
298+ r.map_error(ExceptionA, "net.lew21.pydbus.tests.ErrorA")
299+ r.map_error(ExceptionB, "net.lew21.pydbus.tests.ErrorB")
300+ r.map_error(ExceptionC, "net.lew21.pydbus.tests.ErrorC")
301+
302+ assert r.is_registered_exception(ExceptionA("Test"))
303+ assert r.is_registered_exception(ExceptionB("Test"))
304+ assert r.is_registered_exception(ExceptionC("Test"))
305+ assert not r.is_registered_exception(ExceptionD("Test"))
306+ assert not r.is_registered_exception(ExceptionE("Test"))
307+
308+ assert r.get_dbus_name(ExceptionA("Test")) == "net.lew21.pydbus.tests.ErrorA"
309+ assert r.get_dbus_name(ExceptionB("Test")) == "net.lew21.pydbus.tests.ErrorB"
310+ assert r.get_dbus_name(ExceptionC("Test")) == "net.lew21.pydbus.tests.ErrorC"
311+
312+ assert r.get_exception_class("net.lew21.pydbus.tests.ErrorA") == ExceptionA
313+ assert r.get_exception_class("net.lew21.pydbus.tests.ErrorB") == ExceptionB
314+ assert r.get_exception_class("net.lew21.pydbus.tests.ErrorC") == ExceptionC
315+ assert r.get_exception_class("net.lew21.pydbus.tests.ErrorD") is None
316+ assert r.get_exception_class("net.lew21.pydbus.tests.ErrorE") is None
317+
318+ r.map_by_default(ExceptionD)
319+ assert not r.is_registered_exception(ExceptionD("Test"))
320+ assert r.get_exception_class("net.lew21.pydbus.tests.ErrorD") == ExceptionD
321+ assert r.get_exception_class("net.lew21.pydbus.tests.ErrorE") == ExceptionD
322+
323+
324+def test_transform_message():
325+ r = ErrorRegistration()
326+ n1 = "net.lew21.pydbus.tests.ErrorA"
327+ m1 = "GDBus.Error:net.lew21.pydbus.tests.ErrorA: Message1"
328+
329+ n2 = "net.lew21.pydbus.tests.ErrorB"
330+ m2 = "GDBus.Error:net.lew21.pydbus.tests.ErrorB: Message2"
331+
332+ assert r.transform_message(n1, m1) == "Message1"
333+ assert r.transform_message(n2, m2) == "Message2"
334+ assert r.transform_message(n1, m2) == m2
335+ assert r.transform_message(n2, m1) == m1
336+
337+
338+test_error_mapping()
339+test_transform_message()
340diff --git a/tests/publish_error.py b/tests/publish_error.py
341new file mode 100644
342index 0000000..aa8a18a
343--- /dev/null
344+++ b/tests/publish_error.py
345@@ -0,0 +1,132 @@
346+import sys
347+from threading import Thread
348+from gi.repository import GLib, Gio
349+from pydbus import SessionBus
350+from pydbus.error import register_error, map_error, map_by_default, error_registration
351+
352+import logging
353+logger = logging.getLogger('pydbus.registration')
354+logger.disabled = True
355+
356+loop = GLib.MainLoop()
357+DOMAIN = Gio.DBusError.quark() # TODO: Register new domain.
358+
359+
360+@register_error("net.lew21.pydbus.tests.ErrorA", DOMAIN, 1000)
361+class ExceptionA(Exception):
362+ pass
363+
364+
365+@register_error("net.lew21.pydbus.tests.ErrorB", DOMAIN, 2000)
366+class ExceptionB(Exception):
367+ pass
368+
369+
370+@map_error("org.freedesktop.DBus.Error.InvalidArgs")
371+class ExceptionC(Exception):
372+ pass
373+
374+
375+@map_by_default
376+class ExceptionD(Exception):
377+ pass
378+
379+
380+class ExceptionE(Exception):
381+ pass
382+
383+
384+class TestObject(object):
385+ '''
386+<node>
387+ <interface name='net.lew21.pydbus.tests.TestInterface'>
388+ <method name='RaiseA'>
389+ <arg type='s' name='msg' direction='in'/>
390+ </method>
391+ <method name='RaiseB'>
392+ <arg type='s' name='msg' direction='in'/>
393+ </method>
394+ <method name='RaiseD'>
395+ <arg type='s' name='msg' direction='in'/>
396+ </method>
397+ <method name='RaiseE'>
398+ <arg type='s' name='msg' direction='in'/>
399+ </method>
400+ </interface>
401+</node>
402+ '''
403+
404+ def RaiseA(self, msg):
405+ raise ExceptionA(msg)
406+
407+ def RaiseB(self, msg):
408+ raise ExceptionB(msg)
409+
410+ def RaiseD(self, msg):
411+ raise ExceptionD(msg)
412+
413+ def RaiseE(self, msg):
414+ raise ExceptionE(msg)
415+
416+bus = SessionBus()
417+
418+with bus.publish("net.lew21.pydbus.tests.Test", TestObject()):
419+ remote = bus.get("net.lew21.pydbus.tests.Test")
420+
421+ def t_func():
422+ # Test new registered errors.
423+ try:
424+ remote.RaiseA("Test A")
425+ except ExceptionA as e:
426+ assert str(e) == "Test A"
427+
428+ try:
429+ remote.RaiseB("Test B")
430+ except ExceptionB as e:
431+ assert str(e) == "Test B"
432+
433+ # Test mapped errors.
434+ try:
435+ remote.Get("net.lew21.pydbus.tests.TestInterface", "Foo")
436+ except ExceptionC as e:
437+ assert str(e) == "No such property 'Foo'"
438+
439+ # Test default errors.
440+ try:
441+ remote.RaiseD("Test D")
442+ except ExceptionD as e:
443+ assert str(e) == "Test D"
444+
445+ try:
446+ remote.RaiseE("Test E")
447+ except ExceptionD as e:
448+ assert str(e) == "Test E"
449+
450+ # Test with no default errors.
451+ error_registration.map_by_default(None)
452+
453+ try:
454+ remote.RaiseD("Test D")
455+ except Exception as e:
456+ assert not isinstance(e, ExceptionD)
457+
458+ try:
459+ remote.RaiseE("Test E")
460+ except Exception as e:
461+ assert not isinstance(e, ExceptionD)
462+ assert not isinstance(e, ExceptionE)
463+
464+ loop.quit()
465+
466+ t = Thread(None, t_func)
467+ t.daemon = True
468+
469+ def handle_timeout():
470+ print("ERROR: Timeout.")
471+ sys.exit(1)
472+
473+ GLib.timeout_add_seconds(4, handle_timeout)
474+
475+ t.start()
476+ loop.run()
477+ t.join()
478diff --git a/tests/run.sh b/tests/run.sh
479index 271c58a..a08baf8 100755
480--- a/tests/run.sh
481+++ b/tests/run.sh
482@@ -10,10 +10,11 @@ PYTHON=${1:-python}
483
484 "$PYTHON" $TESTS_DIR/context.py
485 "$PYTHON" $TESTS_DIR/identifier.py
486+"$PYTHON" $TESTS_DIR/error.py
487 if [ "$2" != "dontpublish" ]
488 then
489 "$PYTHON" $TESTS_DIR/publish.py
490 "$PYTHON" $TESTS_DIR/publish_properties.py
491 "$PYTHON" $TESTS_DIR/publish_multiface.py
492 "$PYTHON" $TESTS_DIR/publish_async.py
493 fi
494--
4952.13.5