Skip to content

Commit 7edf99f

Browse files
bluetoothbotclaude
andauthored
feat(errors): add DBusFastError common base class (#634)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 54b6825 commit 7edf99f

3 files changed

Lines changed: 177 additions & 13 deletions

File tree

src/dbus_fast/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .errors import (
1414
AuthError,
1515
DBusError,
16+
DBusFastError,
1617
InterfaceNotFoundError,
1718
InvalidAddressError,
1819
InvalidBusNameError,
@@ -44,6 +45,7 @@
4445
"AuthError",
4546
"BusType",
4647
"DBusError",
48+
"DBusFastError",
4749
"ErrorType",
4850
"InterfaceNotFoundError",
4951
"InvalidAddressError",

src/dbus_fast/errors.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,61 @@
1-
class SignatureBodyMismatchError(ValueError):
1+
class DBusFastError(Exception):
2+
"""Common base class for all dbus-fast exceptions.
3+
4+
Catch this to handle any error raised by dbus-fast regardless of which
5+
Python built-in exception type the specific error also derives from.
6+
Existing ``except ValueError`` / ``except TypeError`` handlers continue
7+
to work because individual error classes still inherit from those.
8+
"""
9+
10+
11+
class SignatureBodyMismatchError(ValueError, DBusFastError):
212
pass
313

414

5-
class InvalidSignatureError(ValueError):
15+
class InvalidSignatureError(ValueError, DBusFastError):
616
pass
717

818

9-
class InvalidAddressError(ValueError):
19+
class InvalidAddressError(ValueError, DBusFastError):
1020
pass
1121

1222

13-
class AuthError(Exception):
23+
class AuthError(DBusFastError):
1424
pass
1525

1626

17-
class InvalidMessageError(ValueError):
27+
class InvalidMessageError(ValueError, DBusFastError):
1828
pass
1929

2030

21-
class InvalidIntrospectionError(ValueError):
31+
class InvalidIntrospectionError(ValueError, DBusFastError):
2232
pass
2333

2434

25-
class InterfaceNotFoundError(Exception):
35+
class InterfaceNotFoundError(DBusFastError):
2636
pass
2737

2838

29-
class SignalDisabledError(Exception):
39+
class SignalDisabledError(DBusFastError):
3040
pass
3141

3242

33-
class InvalidBusNameError(TypeError):
43+
class InvalidBusNameError(TypeError, DBusFastError):
3444
def __init__(self, name: str) -> None:
3545
super().__init__(f"invalid bus name: {name}")
3646

3747

38-
class InvalidObjectPathError(TypeError):
48+
class InvalidObjectPathError(TypeError, DBusFastError):
3949
def __init__(self, path: str) -> None:
4050
super().__init__(f"invalid object path: {path}")
4151

4252

43-
class InvalidInterfaceNameError(TypeError):
53+
class InvalidInterfaceNameError(TypeError, DBusFastError):
4454
def __init__(self, name: str) -> None:
4555
super().__init__(f"invalid interface name: {name}")
4656

4757

48-
class InvalidMemberNameError(TypeError):
58+
class InvalidMemberNameError(TypeError, DBusFastError):
4959
def __init__(self, member: str) -> None:
5060
super().__init__(f"invalid member name: {member}")
5161

@@ -55,7 +65,7 @@ def __init__(self, member: str) -> None:
5565
from .validators import assert_interface_name_valid # noqa: E402
5666

5767

58-
class DBusError(Exception):
68+
class DBusError(DBusFastError):
5969
def __init__(
6070
self, type_: ErrorType | str, text: str, reply: Message | None = None
6171
) -> None:

tests/test_errors.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""Tests for the exception hierarchy in ``dbus_fast.errors``.
2+
3+
The hierarchy was introduced to let callers catch every dbus-fast error with
4+
``except DBusFastError`` without giving up the historical ability to catch
5+
specific built-in types like ``ValueError`` or ``TypeError`` for individual
6+
classes (see GH #507).
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import pytest
12+
13+
from dbus_fast import (
14+
AuthError,
15+
DBusError,
16+
DBusFastError,
17+
InterfaceNotFoundError,
18+
InvalidAddressError,
19+
InvalidBusNameError,
20+
InvalidInterfaceNameError,
21+
InvalidIntrospectionError,
22+
InvalidMemberNameError,
23+
InvalidMessageError,
24+
InvalidObjectPathError,
25+
InvalidSignatureError,
26+
SignalDisabledError,
27+
SignatureBodyMismatchError,
28+
)
29+
30+
_ALL_ERRORS = [
31+
AuthError,
32+
DBusError,
33+
InterfaceNotFoundError,
34+
InvalidAddressError,
35+
InvalidBusNameError,
36+
InvalidInterfaceNameError,
37+
InvalidIntrospectionError,
38+
InvalidMemberNameError,
39+
InvalidMessageError,
40+
InvalidObjectPathError,
41+
InvalidSignatureError,
42+
SignalDisabledError,
43+
SignatureBodyMismatchError,
44+
]
45+
46+
47+
@pytest.mark.parametrize("err_cls", _ALL_ERRORS)
48+
def test_all_errors_share_dbus_fast_base(err_cls: type[BaseException]) -> None:
49+
assert issubclass(err_cls, DBusFastError)
50+
assert issubclass(err_cls, Exception)
51+
52+
53+
@pytest.mark.parametrize(
54+
"err_cls",
55+
[
56+
SignatureBodyMismatchError,
57+
InvalidSignatureError,
58+
InvalidAddressError,
59+
InvalidMessageError,
60+
InvalidIntrospectionError,
61+
],
62+
)
63+
def test_value_error_subclasses_preserved(err_cls: type[BaseException]) -> None:
64+
assert issubclass(err_cls, ValueError)
65+
66+
67+
@pytest.mark.parametrize(
68+
"err_cls",
69+
[
70+
InvalidBusNameError,
71+
InvalidObjectPathError,
72+
InvalidInterfaceNameError,
73+
InvalidMemberNameError,
74+
],
75+
)
76+
def test_type_error_subclasses_preserved(err_cls: type[BaseException]) -> None:
77+
assert issubclass(err_cls, TypeError)
78+
79+
80+
def test_value_error_subclass_mro_order() -> None:
81+
"""Built-in ``ValueError`` must precede ``DBusFastError`` in the MRO.
82+
83+
This ordering ensures ``super().__init__`` routes through
84+
``ValueError.__init__`` rather than skipping straight to
85+
``Exception.__init__``, preserving the behavior of the historical
86+
single-inheritance ``ValueError`` subclasses.
87+
"""
88+
mro = InvalidSignatureError.__mro__
89+
assert mro.index(ValueError) < mro.index(DBusFastError)
90+
91+
92+
def test_type_error_subclass_mro_order() -> None:
93+
"""Built-in ``TypeError`` must precede ``DBusFastError`` in the MRO.
94+
95+
Same rationale as the ``ValueError`` MRO test: keeps ``super().__init__``
96+
routing through ``TypeError.__init__`` to match the original
97+
single-inheritance ``TypeError`` subclasses.
98+
"""
99+
mro = InvalidBusNameError.__mro__
100+
assert mro.index(TypeError) < mro.index(DBusFastError)
101+
102+
103+
def test_dbus_fast_error_catches_value_error_subclass() -> None:
104+
with pytest.raises(DBusFastError):
105+
raise SignatureBodyMismatchError("body mismatch")
106+
107+
108+
def test_dbus_fast_error_catches_type_error_subclass() -> None:
109+
with pytest.raises(DBusFastError):
110+
raise InvalidBusNameError("not.a.valid.bus.name!")
111+
112+
113+
def test_value_error_still_catches_signature_errors() -> None:
114+
with pytest.raises(ValueError):
115+
raise InvalidSignatureError("bad signature")
116+
117+
118+
def test_type_error_still_catches_name_errors() -> None:
119+
with pytest.raises(TypeError):
120+
raise InvalidMemberNameError("1bad")
121+
122+
123+
def test_invalid_bus_name_error_message_unchanged() -> None:
124+
err = InvalidBusNameError("bad")
125+
assert str(err) == "invalid bus name: bad"
126+
127+
128+
def test_invalid_object_path_error_message_unchanged() -> None:
129+
err = InvalidObjectPathError("nope")
130+
assert str(err) == "invalid object path: nope"
131+
132+
133+
def test_invalid_interface_name_error_message_unchanged() -> None:
134+
err = InvalidInterfaceNameError("bad.iface")
135+
assert str(err) == "invalid interface name: bad.iface"
136+
137+
138+
def test_invalid_member_name_error_message_unchanged() -> None:
139+
err = InvalidMemberNameError("1bad")
140+
assert str(err) == "invalid member name: 1bad"
141+
142+
143+
def test_dbus_error_inherits_dbus_fast_error() -> None:
144+
err = DBusError("org.freedesktop.DBus.Error.Failed", "boom")
145+
assert isinstance(err, DBusFastError)
146+
assert err.type == "org.freedesktop.DBus.Error.Failed"
147+
assert err.text == "boom"
148+
149+
150+
def test_dbus_fast_error_directly_raisable() -> None:
151+
with pytest.raises(DBusFastError):
152+
raise DBusFastError("plain base class")

0 commit comments

Comments
 (0)