aboutsummaryrefslogtreecommitdiffstats
path: root/meta-python/recipes-devtools/python/python3-pydbus/0003-Support-transformation-between-D-Bus-errors-and-exce.patch
blob: a1b8a6c38ceac17291dfcab659cd31bcdf9aa993 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
From 773858e1afd21cdf3ceef2cd35509f0b4882bf16 Mon Sep 17 00:00:00 2001
From: Vendula Poncova <vponcova@redhat.com>
Date: Tue, 1 Aug 2017 16:54:24 +0200
Subject: [PATCH 3/3] Support transformation between D-Bus errors and
 exceptions.

Exceptions can be registered with decorators, raised in a remote
method and recreated after return from the remote call.

Adapted from Fedora [https://src.fedoraproject.org/cgit/rpms/python-pydbus.git/]

Upstream-Status: Inactive-Upstream (Last release 12/18/2016; Last commit 05/6/2018)

Signed-off-by: Derek Straka <derek@asterius.io>
---
 doc/tutorial.rst       |  47 ++++++++++++++++++
 pydbus/error.py        |  97 ++++++++++++++++++++++++++++++++++++
 pydbus/proxy_method.py |  18 +++++--
 pydbus/registration.py |  16 ++++--
 tests/error.py         |  67 +++++++++++++++++++++++++
 tests/publish_error.py | 132 +++++++++++++++++++++++++++++++++++++++++++++++++
 tests/run.sh           |   2 +
 7 files changed, 371 insertions(+), 8 deletions(-)
 create mode 100644 pydbus/error.py
 create mode 100644 tests/error.py
 create mode 100644 tests/publish_error.py

diff --git a/doc/tutorial.rst b/doc/tutorial.rst
index b8479cf..7fe55e1 100644
--- a/doc/tutorial.rst
+++ b/doc/tutorial.rst
@@ -341,6 +341,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
 ==========
 
diff --git a/pydbus/error.py b/pydbus/error.py
new file mode 100644
index 0000000..aaa3510
--- /dev/null
+++ b/pydbus/error.py
@@ -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()
diff --git a/pydbus/proxy_method.py b/pydbus/proxy_method.py
index 442fe07..a73f9eb 100644
--- a/pydbus/proxy_method.py
+++ b/pydbus/proxy_method.py
@@ -2,6 +2,7 @@ from gi.repository import GLib
 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
@@ -87,9 +88,20 @@ class ProxyMethod(object):
 			call_args += (self._finish_async_call, (callback, callback_args))
 			instance._bus.con.call(*call_args)
 			return None
+
 		else:
-			ret = instance._bus.con.call_sync(*call_args)
-			return self._unpack_return(ret)
+			result = None
+			error = None
+
+			try:
+				result = instance._bus.con.call_sync(*call_args)
+			except Exception as e:
+				error = error_registration.transform_exception(e)
+
+			if error:
+				raise error
+
+			return self._unpack_return(result)
 
 	def _unpack_return(self, values):
 		ret = values.unpack()
@@ -108,7 +120,7 @@ class ProxyMethod(object):
 			ret = source.call_finish(result)
 			return_args = self._unpack_return(ret)
 		except Exception as err:
-			error = err
+			error = error_registration.transform_exception(err)
 
 		callback, callback_args = user_data
 		callback(*callback_args, returned=return_args, error=error)
diff --git a/pydbus/registration.py b/pydbus/registration.py
index f531539..1d2cbcb 100644
--- a/pydbus/registration.py
+++ b/pydbus/registration.py
@@ -5,6 +5,7 @@ from . import generic
 from .exitable import ExitableWithAliases
 from functools import partial
 from .method_call_context import MethodCallContext
+from .error import error_registration
 import logging
 
 try:
@@ -91,11 +92,16 @@ class ObjectWrapper(ExitableWithAliases("unwrap")):
 			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]
diff --git a/tests/error.py b/tests/error.py
new file mode 100644
index 0000000..3ec507d
--- /dev/null
+++ b/tests/error.py
@@ -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()
diff --git a/tests/publish_error.py b/tests/publish_error.py
new file mode 100644
index 0000000..aa8a18a
--- /dev/null
+++ b/tests/publish_error.py
@@ -0,0 +1,132 @@
+import sys
+from threading import Thread
+from gi.repository import GLib, Gio
+from pydbus import SessionBus
+from pydbus.error import register_error, map_error, map_by_default, error_registration
+
+import logging
+logger = logging.getLogger('pydbus.registration')
+logger.disabled = True
+
+loop = GLib.MainLoop()
+DOMAIN = Gio.DBusError.quark()  # TODO: Register new domain.
+
+
+@register_error("net.lew21.pydbus.tests.ErrorA", DOMAIN, 1000)
+class ExceptionA(Exception):
+	pass
+
+
+@register_error("net.lew21.pydbus.tests.ErrorB", DOMAIN, 2000)
+class ExceptionB(Exception):
+	pass
+
+
+@map_error("org.freedesktop.DBus.Error.InvalidArgs")
+class ExceptionC(Exception):
+	pass
+
+
+@map_by_default
+class ExceptionD(Exception):
+	pass
+
+
+class ExceptionE(Exception):
+	pass
+
+
+class TestObject(object):
+	'''
+<node>
+	<interface name='net.lew21.pydbus.tests.TestInterface'>
+		<method name='RaiseA'>
+			<arg type='s' name='msg' direction='in'/>
+		</method>
+		<method name='RaiseB'>
+			<arg type='s' name='msg' direction='in'/>
+		</method>
+		<method name='RaiseD'>
+			<arg type='s' name='msg' direction='in'/>
+		</method>
+		<method name='RaiseE'>
+			<arg type='s' name='msg' direction='in'/>
+		</method>
+	</interface>
+</node>
+	'''
+
+	def RaiseA(self, msg):
+		raise ExceptionA(msg)
+
+	def RaiseB(self, msg):
+		raise ExceptionB(msg)
+
+	def RaiseD(self, msg):
+		raise ExceptionD(msg)
+
+	def RaiseE(self, msg):
+		raise ExceptionE(msg)
+
+bus = SessionBus()
+
+with bus.publish("net.lew21.pydbus.tests.Test", TestObject()):
+	remote = bus.get("net.lew21.pydbus.tests.Test")
+
+	def t_func():
+		# Test new registered errors.
+		try:
+			remote.RaiseA("Test A")
+		except ExceptionA as e:
+			assert str(e) == "Test A"
+
+		try:
+			remote.RaiseB("Test B")
+		except ExceptionB as e:
+			assert str(e) == "Test B"
+
+		# Test mapped errors.
+		try:
+			remote.Get("net.lew21.pydbus.tests.TestInterface", "Foo")
+		except ExceptionC as e:
+			assert str(e) == "No such property 'Foo'"
+
+		# Test default errors.
+		try:
+			remote.RaiseD("Test D")
+		except ExceptionD as e:
+			assert str(e) == "Test D"
+
+		try:
+			remote.RaiseE("Test E")
+		except ExceptionD as e:
+			assert str(e) == "Test E"
+
+		# Test with no default errors.
+		error_registration.map_by_default(None)
+
+		try:
+			remote.RaiseD("Test D")
+		except Exception as e:
+			assert not isinstance(e, ExceptionD)
+
+		try:
+			remote.RaiseE("Test E")
+		except Exception as e:
+			assert not isinstance(e, ExceptionD)
+			assert not isinstance(e, ExceptionE)
+
+		loop.quit()
+
+	t = Thread(None, t_func)
+	t.daemon = True
+
+	def handle_timeout():
+		print("ERROR: Timeout.")
+		sys.exit(1)
+
+	GLib.timeout_add_seconds(4, handle_timeout)
+
+	t.start()
+	loop.run()
+	t.join()
diff --git a/tests/run.sh b/tests/run.sh
index 271c58a..a08baf8 100755
--- a/tests/run.sh
+++ b/tests/run.sh
@@ -10,10 +10,11 @@ PYTHON=${1:-python}
 
 "$PYTHON" $TESTS_DIR/context.py
 "$PYTHON" $TESTS_DIR/identifier.py
+"$PYTHON" $TESTS_DIR/error.py
 if [ "$2" != "dontpublish" ]
 then
 	"$PYTHON" $TESTS_DIR/publish.py
 	"$PYTHON" $TESTS_DIR/publish_properties.py
 	"$PYTHON" $TESTS_DIR/publish_multiface.py
 	"$PYTHON" $TESTS_DIR/publish_async.py
 fi
-- 
2.13.5