From 1f62f394e23464465928590710e0c491560484ed Mon Sep 17 00:00:00 2001 From: Rik Bouwmeester Date: Mon, 9 Mar 2026 14:56:35 +0100 Subject: [PATCH 1/2] Register custom exceptions for Python bindings --- cflib2/__init__.py | 32 +++++++++++++ cflib2/_rust.pyi | 105 +++++++++++++++++++++++++++++++++++++++++++ rust/src/error.rs | 62 +++++++++++++++++++++++-- rust/src/lib.rs | 1 + scripts/fix_stubs.py | 6 +++ 5 files changed, 202 insertions(+), 4 deletions(-) diff --git a/cflib2/__init__.py b/cflib2/__init__.py index 322777f..ec86bea 100644 --- a/cflib2/__init__.py +++ b/cflib2/__init__.py @@ -29,6 +29,22 @@ NoTocCache, InMemoryTocCache, FileTocCache, + # Exceptions + CrazyflieError, + ProtocolVersionNotSupportedError, + ProtocolError, + ParamError, + LogError, + ConversionError, + LinkError, + DisconnectedError, + VariableNotFoundError, + SystemError, + AppchannelPacketTooLargeError, + InvalidArgumentError, + TimeoutError, + MemoryError, + InvalidParameterError, ) __all__ = [ @@ -37,4 +53,20 @@ "NoTocCache", "InMemoryTocCache", "FileTocCache", + # Exceptions + "CrazyflieError", + "ProtocolVersionNotSupportedError", + "ProtocolError", + "ParamError", + "LogError", + "ConversionError", + "LinkError", + "DisconnectedError", + "VariableNotFoundError", + "SystemError", + "AppchannelPacketTooLargeError", + "InvalidArgumentError", + "TimeoutError", + "MemoryError", + "InvalidParameterError", ] diff --git a/cflib2/_rust.pyi b/cflib2/_rust.pyi index da8160c..4fff888 100644 --- a/cflib2/_rust.pyi +++ b/cflib2/_rust.pyi @@ -40,6 +40,13 @@ class AppChannel: * List of received data packets (each up to 31 bytes) """ +class AppchannelPacketTooLargeError(CrazyflieError): + r""" + App channel packet exceeds MTU. + """ + + ... + @typing.final class Commander: r""" @@ -273,6 +280,13 @@ class Console: List of console output lines (up to 100 with 10ms timeout) """ +class ConversionError(CrazyflieError): + r""" + Value conversion error. + """ + + ... + @typing.final class Crazyflie: r""" @@ -345,6 +359,20 @@ class Crazyflie: def __str__(self) -> builtins.str: ... def __repr__(self) -> builtins.str: ... +class CrazyflieError(builtins.Exception): + r""" + Base exception for all Crazyflie errors. + """ + + ... + +class DisconnectedError(CrazyflieError): + r""" + Crazyflie is disconnected. + """ + + ... + @typing.final class EmergencyControl: r""" @@ -657,6 +685,20 @@ class InMemoryTocCache: Get the number of cached TOCs """ +class InvalidArgumentError(CrazyflieError): + r""" + Invalid argument. + """ + + ... + +class InvalidParameterError(CrazyflieError): + r""" + Invalid parameter. + """ + + ... + @typing.final class Lighthouse: r""" @@ -763,6 +805,13 @@ class LinkContext: List of URIs found """ +class LinkError(CrazyflieError): + r""" + Crazyflie link error. + """ + + ... + @typing.final class Localization: r""" @@ -909,6 +958,13 @@ class LogData: Dictionary of variable name to value """ +class LogError(CrazyflieError): + r""" + Log subsystem error. + """ + + ... + @typing.final class LogStream: r""" @@ -1028,6 +1084,13 @@ class Memory: * `length` - Number of bytes to read """ +class MemoryError(CrazyflieError): + r""" + Memory subsystem error. + """ + + ... + @typing.final class NoTocCache: r""" @@ -1181,6 +1244,13 @@ class Param: * `name` - Parameter name in format "group.name" """ +class ParamError(CrazyflieError): + r""" + Parameter subsystem error. + """ + + ... + @typing.final class PersistentParamState: r""" @@ -1330,3 +1400,38 @@ class Poly4D: def __new__( cls, duration: builtins.float, x: Poly, y: Poly, z: Poly, yaw: Poly ) -> Poly4D: ... + +class ProtocolError(CrazyflieError): + r""" + Unexpected protocol error. + """ + + ... + +class ProtocolVersionNotSupportedError(CrazyflieError): + r""" + Protocol version not supported. + """ + + ... + +class SystemError(CrazyflieError): + r""" + Async executor error. + """ + + ... + +class TimeoutError(CrazyflieError): + r""" + Operation timed out. + """ + + ... + +class VariableNotFoundError(CrazyflieError): + r""" + Variable not found in TOC. + """ + + ... diff --git a/rust/src/error.rs b/rust/src/error.rs index 1bee653..0b41f6b 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -4,7 +4,7 @@ // | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/ // +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/ // -// Copyright (C) 2025 Bitcraze AB +// Copyright (C) 2026 Bitcraze AB // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -21,10 +21,64 @@ //! Error conversion utilities for Python bindings -use pyo3::exceptions::PyRuntimeError; -use pyo3::PyErr; +use pyo3::exceptions::PyException; +use pyo3::prelude::*; + +pyo3_stub_gen::create_exception!(cflib2._rust, CrazyflieError, PyException, "Base exception for all Crazyflie errors."); +pyo3_stub_gen::create_exception!(cflib2._rust, ProtocolVersionNotSupportedError, CrazyflieError, "Protocol version not supported."); +pyo3_stub_gen::create_exception!(cflib2._rust, ProtocolError, CrazyflieError, "Unexpected protocol error."); +pyo3_stub_gen::create_exception!(cflib2._rust, ParamError, CrazyflieError, "Parameter subsystem error."); +pyo3_stub_gen::create_exception!(cflib2._rust, LogError, CrazyflieError, "Log subsystem error."); +pyo3_stub_gen::create_exception!(cflib2._rust, ConversionError, CrazyflieError, "Value conversion error."); +pyo3_stub_gen::create_exception!(cflib2._rust, LinkError, CrazyflieError, "Crazyflie link error."); +pyo3_stub_gen::create_exception!(cflib2._rust, DisconnectedError, CrazyflieError, "Crazyflie is disconnected."); +pyo3_stub_gen::create_exception!(cflib2._rust, VariableNotFoundError, CrazyflieError, "Variable not found in TOC."); +pyo3_stub_gen::create_exception!(cflib2._rust, SystemError, CrazyflieError, "Async executor error."); +pyo3_stub_gen::create_exception!(cflib2._rust, AppchannelPacketTooLargeError, CrazyflieError, "App channel packet exceeds MTU."); +pyo3_stub_gen::create_exception!(cflib2._rust, InvalidArgumentError, CrazyflieError, "Invalid argument."); +pyo3_stub_gen::create_exception!(cflib2._rust, TimeoutError, CrazyflieError, "Operation timed out."); +pyo3_stub_gen::create_exception!(cflib2._rust, MemoryError, CrazyflieError, "Memory subsystem error."); +pyo3_stub_gen::create_exception!(cflib2._rust, InvalidParameterError, CrazyflieError, "Invalid parameter."); + +/// Register all custom exception types with the Python module +pub fn register_exceptions(m: &Bound<'_, PyModule>) -> PyResult<()> { + let py = m.py(); + m.add("CrazyflieError", py.get_type::())?; + m.add("ProtocolVersionNotSupportedError", py.get_type::())?; + m.add("ProtocolError", py.get_type::())?; + m.add("ParamError", py.get_type::())?; + m.add("LogError", py.get_type::())?; + m.add("ConversionError", py.get_type::())?; + m.add("LinkError", py.get_type::())?; + m.add("DisconnectedError", py.get_type::())?; + m.add("VariableNotFoundError", py.get_type::())?; + m.add("SystemError", py.get_type::())?; + m.add("AppchannelPacketTooLargeError", py.get_type::())?; + m.add("InvalidArgumentError", py.get_type::())?; + m.add("TimeoutError", py.get_type::())?; + m.add("MemoryError", py.get_type::())?; + m.add("InvalidParameterError", py.get_type::())?; + Ok(()) +} /// Convert Rust crazyflie_lib errors to Python exceptions pub fn to_pyerr(err: crazyflie_lib::Error) -> PyErr { - PyRuntimeError::new_err(format!("Crazyflie error: {:?}", err)) + use crazyflie_lib::Error::*; + let msg = err.to_string(); + match err { + ProtocolVersionNotSupported { .. } => ProtocolVersionNotSupportedError::new_err(msg), + ProtocolError(_) => self::ProtocolError::new_err(msg), + ParamError(_) => self::ParamError::new_err(msg), + LogError(_) => self::LogError::new_err(msg), + ConversionError(_) => self::ConversionError::new_err(msg), + LinkError(_) => self::LinkError::new_err(msg), + Disconnected => DisconnectedError::new_err(msg), + VariableNotFound => VariableNotFoundError::new_err(msg), + SystemError(_) => self::SystemError::new_err(msg), + AppchannelPacketTooLarge => AppchannelPacketTooLargeError::new_err(msg), + InvalidArgument(_) => InvalidArgumentError::new_err(msg), + Timeout => self::TimeoutError::new_err(msg), + MemoryError(_) => self::MemoryError::new_err(msg), + InvalidParameter(_) => InvalidParameterError::new_err(msg), + } } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 7cae17f..604e7d6 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -72,6 +72,7 @@ fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + error::register_exceptions(m)?; Ok(()) } diff --git a/scripts/fix_stubs.py b/scripts/fix_stubs.py index cd67952..9be8e6d 100644 --- a/scripts/fix_stubs.py +++ b/scripts/fix_stubs.py @@ -99,6 +99,12 @@ def fix_stubs(content: str) -> str: if "collections.abc." not in result.split("import collections.abc\n")[-1]: result = result.replace("import collections.abc\n", "") + # pyo3_stub_gen's create_exception! macro qualifies custom exception base + # classes with `builtins.` (e.g. `builtins.CrazyflieError`). Only the + # root `CrazyflieError(builtins.Exception)` is correct; subclasses must + # reference the module-level name directly. + result = result.replace("builtins.CrazyflieError", "CrazyflieError") + return result From a22625ade16f8d277e84c70332ff77883a85925a Mon Sep 17 00:00:00 2001 From: Rik Bouwmeester Date: Mon, 9 Mar 2026 15:39:46 +0100 Subject: [PATCH 2/2] Add unit tests for custom exception hierarchy --- tests/test_exceptions.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/test_exceptions.py diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..bf861c9 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,35 @@ +# ,---------, ____ _ __ +# | ,-^-, | / __ )(_) /_______________ _____ ___ +# | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ +# | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/ +# +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/ +# +# Copyright (C) 2026 Bitcraze AB +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import pytest + +import cflib2 + + +EXCEPTION_NAMES = [name for name in cflib2.__all__ if name.endswith("Error")] + + +class TestExceptionHierarchy: + """Verify that all custom exceptions inherit from CrazyflieError.""" + + @pytest.mark.parametrize("name", EXCEPTION_NAMES) + def test_exception_is_subclass_of_crazyflie_error(self, name: str) -> None: + exc_class = getattr(cflib2, name) + assert issubclass(exc_class, cflib2.CrazyflieError)