Skip to content

Commit b2948b7

Browse files
authored
Merge pull request #6 from bitcraze/rik/exceptions
Register custom exceptions for Python bindings
2 parents bd8dcff + a22625a commit b2948b7

6 files changed

Lines changed: 237 additions & 4 deletions

File tree

cflib2/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@
2929
NoTocCache,
3030
InMemoryTocCache,
3131
FileTocCache,
32+
# Exceptions
33+
CrazyflieError,
34+
ProtocolVersionNotSupportedError,
35+
ProtocolError,
36+
ParamError,
37+
LogError,
38+
ConversionError,
39+
LinkError,
40+
DisconnectedError,
41+
VariableNotFoundError,
42+
SystemError,
43+
AppchannelPacketTooLargeError,
44+
InvalidArgumentError,
45+
TimeoutError,
46+
MemoryError,
47+
InvalidParameterError,
3248
)
3349

3450
__all__ = [
@@ -37,4 +53,20 @@
3753
"NoTocCache",
3854
"InMemoryTocCache",
3955
"FileTocCache",
56+
# Exceptions
57+
"CrazyflieError",
58+
"ProtocolVersionNotSupportedError",
59+
"ProtocolError",
60+
"ParamError",
61+
"LogError",
62+
"ConversionError",
63+
"LinkError",
64+
"DisconnectedError",
65+
"VariableNotFoundError",
66+
"SystemError",
67+
"AppchannelPacketTooLargeError",
68+
"InvalidArgumentError",
69+
"TimeoutError",
70+
"MemoryError",
71+
"InvalidParameterError",
4072
]

cflib2/_rust.pyi

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ class AppChannel:
4040
* List of received data packets (each up to 31 bytes)
4141
"""
4242

43+
class AppchannelPacketTooLargeError(CrazyflieError):
44+
r"""
45+
App channel packet exceeds MTU.
46+
"""
47+
48+
...
49+
4350
@typing.final
4451
class Commander:
4552
r"""
@@ -273,6 +280,13 @@ class Console:
273280
List of console output lines (up to 100 with 10ms timeout)
274281
"""
275282

283+
class ConversionError(CrazyflieError):
284+
r"""
285+
Value conversion error.
286+
"""
287+
288+
...
289+
276290
@typing.final
277291
class Crazyflie:
278292
r"""
@@ -345,6 +359,20 @@ class Crazyflie:
345359
def __str__(self) -> builtins.str: ...
346360
def __repr__(self) -> builtins.str: ...
347361

362+
class CrazyflieError(builtins.Exception):
363+
r"""
364+
Base exception for all Crazyflie errors.
365+
"""
366+
367+
...
368+
369+
class DisconnectedError(CrazyflieError):
370+
r"""
371+
Crazyflie is disconnected.
372+
"""
373+
374+
...
375+
348376
@typing.final
349377
class EmergencyControl:
350378
r"""
@@ -657,6 +685,20 @@ class InMemoryTocCache:
657685
Get the number of cached TOCs
658686
"""
659687

688+
class InvalidArgumentError(CrazyflieError):
689+
r"""
690+
Invalid argument.
691+
"""
692+
693+
...
694+
695+
class InvalidParameterError(CrazyflieError):
696+
r"""
697+
Invalid parameter.
698+
"""
699+
700+
...
701+
660702
@typing.final
661703
class Lighthouse:
662704
r"""
@@ -763,6 +805,13 @@ class LinkContext:
763805
List of URIs found
764806
"""
765807

808+
class LinkError(CrazyflieError):
809+
r"""
810+
Crazyflie link error.
811+
"""
812+
813+
...
814+
766815
@typing.final
767816
class Localization:
768817
r"""
@@ -909,6 +958,13 @@ class LogData:
909958
Dictionary of variable name to value
910959
"""
911960

961+
class LogError(CrazyflieError):
962+
r"""
963+
Log subsystem error.
964+
"""
965+
966+
...
967+
912968
@typing.final
913969
class LogStream:
914970
r"""
@@ -1028,6 +1084,13 @@ class Memory:
10281084
* `length` - Number of bytes to read
10291085
"""
10301086

1087+
class MemoryError(CrazyflieError):
1088+
r"""
1089+
Memory subsystem error.
1090+
"""
1091+
1092+
...
1093+
10311094
@typing.final
10321095
class NoTocCache:
10331096
r"""
@@ -1181,6 +1244,13 @@ class Param:
11811244
* `name` - Parameter name in format "group.name"
11821245
"""
11831246

1247+
class ParamError(CrazyflieError):
1248+
r"""
1249+
Parameter subsystem error.
1250+
"""
1251+
1252+
...
1253+
11841254
@typing.final
11851255
class PersistentParamState:
11861256
r"""
@@ -1330,3 +1400,38 @@ class Poly4D:
13301400
def __new__(
13311401
cls, duration: builtins.float, x: Poly, y: Poly, z: Poly, yaw: Poly
13321402
) -> Poly4D: ...
1403+
1404+
class ProtocolError(CrazyflieError):
1405+
r"""
1406+
Unexpected protocol error.
1407+
"""
1408+
1409+
...
1410+
1411+
class ProtocolVersionNotSupportedError(CrazyflieError):
1412+
r"""
1413+
Protocol version not supported.
1414+
"""
1415+
1416+
...
1417+
1418+
class SystemError(CrazyflieError):
1419+
r"""
1420+
Async executor error.
1421+
"""
1422+
1423+
...
1424+
1425+
class TimeoutError(CrazyflieError):
1426+
r"""
1427+
Operation timed out.
1428+
"""
1429+
1430+
...
1431+
1432+
class VariableNotFoundError(CrazyflieError):
1433+
r"""
1434+
Variable not found in TOC.
1435+
"""
1436+
1437+
...

rust/src/error.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
55
// +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/
66
//
7-
// Copyright (C) 2025 Bitcraze AB
7+
// Copyright (C) 2026 Bitcraze AB
88
//
99
// This program is free software: you can redistribute it and/or modify
1010
// it under the terms of the GNU General Public License as published by
@@ -21,10 +21,64 @@
2121

2222
//! Error conversion utilities for Python bindings
2323
24-
use pyo3::exceptions::PyRuntimeError;
25-
use pyo3::PyErr;
24+
use pyo3::exceptions::PyException;
25+
use pyo3::prelude::*;
26+
27+
pyo3_stub_gen::create_exception!(cflib2._rust, CrazyflieError, PyException, "Base exception for all Crazyflie errors.");
28+
pyo3_stub_gen::create_exception!(cflib2._rust, ProtocolVersionNotSupportedError, CrazyflieError, "Protocol version not supported.");
29+
pyo3_stub_gen::create_exception!(cflib2._rust, ProtocolError, CrazyflieError, "Unexpected protocol error.");
30+
pyo3_stub_gen::create_exception!(cflib2._rust, ParamError, CrazyflieError, "Parameter subsystem error.");
31+
pyo3_stub_gen::create_exception!(cflib2._rust, LogError, CrazyflieError, "Log subsystem error.");
32+
pyo3_stub_gen::create_exception!(cflib2._rust, ConversionError, CrazyflieError, "Value conversion error.");
33+
pyo3_stub_gen::create_exception!(cflib2._rust, LinkError, CrazyflieError, "Crazyflie link error.");
34+
pyo3_stub_gen::create_exception!(cflib2._rust, DisconnectedError, CrazyflieError, "Crazyflie is disconnected.");
35+
pyo3_stub_gen::create_exception!(cflib2._rust, VariableNotFoundError, CrazyflieError, "Variable not found in TOC.");
36+
pyo3_stub_gen::create_exception!(cflib2._rust, SystemError, CrazyflieError, "Async executor error.");
37+
pyo3_stub_gen::create_exception!(cflib2._rust, AppchannelPacketTooLargeError, CrazyflieError, "App channel packet exceeds MTU.");
38+
pyo3_stub_gen::create_exception!(cflib2._rust, InvalidArgumentError, CrazyflieError, "Invalid argument.");
39+
pyo3_stub_gen::create_exception!(cflib2._rust, TimeoutError, CrazyflieError, "Operation timed out.");
40+
pyo3_stub_gen::create_exception!(cflib2._rust, MemoryError, CrazyflieError, "Memory subsystem error.");
41+
pyo3_stub_gen::create_exception!(cflib2._rust, InvalidParameterError, CrazyflieError, "Invalid parameter.");
42+
43+
/// Register all custom exception types with the Python module
44+
pub fn register_exceptions(m: &Bound<'_, PyModule>) -> PyResult<()> {
45+
let py = m.py();
46+
m.add("CrazyflieError", py.get_type::<CrazyflieError>())?;
47+
m.add("ProtocolVersionNotSupportedError", py.get_type::<ProtocolVersionNotSupportedError>())?;
48+
m.add("ProtocolError", py.get_type::<ProtocolError>())?;
49+
m.add("ParamError", py.get_type::<ParamError>())?;
50+
m.add("LogError", py.get_type::<LogError>())?;
51+
m.add("ConversionError", py.get_type::<ConversionError>())?;
52+
m.add("LinkError", py.get_type::<LinkError>())?;
53+
m.add("DisconnectedError", py.get_type::<DisconnectedError>())?;
54+
m.add("VariableNotFoundError", py.get_type::<VariableNotFoundError>())?;
55+
m.add("SystemError", py.get_type::<SystemError>())?;
56+
m.add("AppchannelPacketTooLargeError", py.get_type::<AppchannelPacketTooLargeError>())?;
57+
m.add("InvalidArgumentError", py.get_type::<InvalidArgumentError>())?;
58+
m.add("TimeoutError", py.get_type::<TimeoutError>())?;
59+
m.add("MemoryError", py.get_type::<MemoryError>())?;
60+
m.add("InvalidParameterError", py.get_type::<InvalidParameterError>())?;
61+
Ok(())
62+
}
2663

2764
/// Convert Rust crazyflie_lib errors to Python exceptions
2865
pub fn to_pyerr(err: crazyflie_lib::Error) -> PyErr {
29-
PyRuntimeError::new_err(format!("Crazyflie error: {:?}", err))
66+
use crazyflie_lib::Error::*;
67+
let msg = err.to_string();
68+
match err {
69+
ProtocolVersionNotSupported { .. } => ProtocolVersionNotSupportedError::new_err(msg),
70+
ProtocolError(_) => self::ProtocolError::new_err(msg),
71+
ParamError(_) => self::ParamError::new_err(msg),
72+
LogError(_) => self::LogError::new_err(msg),
73+
ConversionError(_) => self::ConversionError::new_err(msg),
74+
LinkError(_) => self::LinkError::new_err(msg),
75+
Disconnected => DisconnectedError::new_err(msg),
76+
VariableNotFound => VariableNotFoundError::new_err(msg),
77+
SystemError(_) => self::SystemError::new_err(msg),
78+
AppchannelPacketTooLarge => AppchannelPacketTooLargeError::new_err(msg),
79+
InvalidArgument(_) => InvalidArgumentError::new_err(msg),
80+
Timeout => self::TimeoutError::new_err(msg),
81+
MemoryError(_) => self::MemoryError::new_err(msg),
82+
InvalidParameter(_) => InvalidParameterError::new_err(msg),
83+
}
3084
}

rust/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
7272
m.add_class::<NoTocCache>()?;
7373
m.add_class::<InMemoryTocCache>()?;
7474
m.add_class::<FileTocCache>()?;
75+
error::register_exceptions(m)?;
7576
Ok(())
7677
}
7778

scripts/fix_stubs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ def fix_stubs(content: str) -> str:
9999
if "collections.abc." not in result.split("import collections.abc\n")[-1]:
100100
result = result.replace("import collections.abc\n", "")
101101

102+
# pyo3_stub_gen's create_exception! macro qualifies custom exception base
103+
# classes with `builtins.` (e.g. `builtins.CrazyflieError`). Only the
104+
# root `CrazyflieError(builtins.Exception)` is correct; subclasses must
105+
# reference the module-level name directly.
106+
result = result.replace("builtins.CrazyflieError", "CrazyflieError")
107+
102108
return result
103109

104110

tests/test_exceptions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# ,---------, ____ _ __
2+
# | ,-^-, | / __ )(_) /_______________ _____ ___
3+
# | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \
4+
# | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
5+
# +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/
6+
#
7+
# Copyright (C) 2026 Bitcraze AB
8+
#
9+
# This program is free software: you can redistribute it and/or modify
10+
# it under the terms of the GNU General Public License as published by
11+
# the Free Software Foundation, either version 3 of the License, or
12+
# (at your option) any later version.
13+
#
14+
# This program is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# GNU General Public License for more details.
18+
#
19+
# You should have received a copy of the GNU General Public License
20+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
import pytest
22+
23+
import cflib2
24+
25+
26+
EXCEPTION_NAMES = [name for name in cflib2.__all__ if name.endswith("Error")]
27+
28+
29+
class TestExceptionHierarchy:
30+
"""Verify that all custom exceptions inherit from CrazyflieError."""
31+
32+
@pytest.mark.parametrize("name", EXCEPTION_NAMES)
33+
def test_exception_is_subclass_of_crazyflie_error(self, name: str) -> None:
34+
exc_class = getattr(cflib2, name)
35+
assert issubclass(exc_class, cflib2.CrazyflieError)

0 commit comments

Comments
 (0)