Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions cflib2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@
NoTocCache,
InMemoryTocCache,
FileTocCache,
# Exceptions
CrazyflieError,
ProtocolVersionNotSupportedError,
ProtocolError,
ParamError,
LogError,
ConversionError,
LinkError,
DisconnectedError,
VariableNotFoundError,
SystemError,
AppchannelPacketTooLargeError,
InvalidArgumentError,
Comment thread
gemenerik marked this conversation as resolved.
TimeoutError,
MemoryError,
InvalidParameterError,
)

__all__ = [
Expand All @@ -37,4 +53,20 @@
"NoTocCache",
"InMemoryTocCache",
"FileTocCache",
# Exceptions
"CrazyflieError",
"ProtocolVersionNotSupportedError",
"ProtocolError",
"ParamError",
"LogError",
"ConversionError",
"LinkError",
"DisconnectedError",
"VariableNotFoundError",
"SystemError",
"AppchannelPacketTooLargeError",
"InvalidArgumentError",
"TimeoutError",
"MemoryError",
"InvalidParameterError",
]
105 changes: 105 additions & 0 deletions cflib2/_rust.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -763,6 +805,13 @@ class LinkContext:
List of URIs found
"""

class LinkError(CrazyflieError):
r"""
Crazyflie link error.
"""

...

@typing.final
class Localization:
r"""
Expand Down Expand Up @@ -909,6 +958,13 @@ class LogData:
Dictionary of variable name to value
"""

class LogError(CrazyflieError):
r"""
Log subsystem error.
"""

...

@typing.final
class LogStream:
r"""
Expand Down Expand Up @@ -1028,6 +1084,13 @@ class Memory:
* `length` - Number of bytes to read
"""

class MemoryError(CrazyflieError):
r"""
Memory subsystem error.
"""

...

@typing.final
class NoTocCache:
r"""
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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.
"""

...
62 changes: 58 additions & 4 deletions rust/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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::<CrazyflieError>())?;
m.add("ProtocolVersionNotSupportedError", py.get_type::<ProtocolVersionNotSupportedError>())?;
m.add("ProtocolError", py.get_type::<ProtocolError>())?;
m.add("ParamError", py.get_type::<ParamError>())?;
m.add("LogError", py.get_type::<LogError>())?;
m.add("ConversionError", py.get_type::<ConversionError>())?;
m.add("LinkError", py.get_type::<LinkError>())?;
m.add("DisconnectedError", py.get_type::<DisconnectedError>())?;
m.add("VariableNotFoundError", py.get_type::<VariableNotFoundError>())?;
m.add("SystemError", py.get_type::<SystemError>())?;
m.add("AppchannelPacketTooLargeError", py.get_type::<AppchannelPacketTooLargeError>())?;
m.add("InvalidArgumentError", py.get_type::<InvalidArgumentError>())?;
m.add("TimeoutError", py.get_type::<TimeoutError>())?;
m.add("MemoryError", py.get_type::<MemoryError>())?;
m.add("InvalidParameterError", py.get_type::<InvalidParameterError>())?;
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),
Comment thread
gemenerik marked this conversation as resolved.
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),
}
}
1 change: 1 addition & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<NoTocCache>()?;
m.add_class::<InMemoryTocCache>()?;
m.add_class::<FileTocCache>()?;
error::register_exceptions(m)?;
Ok(())
}

Expand Down
6 changes: 6 additions & 0 deletions scripts/fix_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
35 changes: 35 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
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)
Loading