diff --git a/libp2p/bitswap/__init__.py b/libp2p/bitswap/__init__.py index a6188a794..c429edaf6 100644 --- a/libp2p/bitswap/__init__.py +++ b/libp2p/bitswap/__init__.py @@ -28,7 +28,7 @@ InvalidBlockError, BlockTooLargeError, MessageTooLargeError, - TimeoutError, + BitswapTimeoutError, BlockNotFoundError, BlockUnavailableError, InvalidCIDError, @@ -57,7 +57,7 @@ "InvalidBlockError", "BlockTooLargeError", "MessageTooLargeError", - "TimeoutError", + "BitswapTimeoutError", "BlockNotFoundError", "BlockUnavailableError", "InvalidCIDError", diff --git a/libp2p/bitswap/client.py b/libp2p/bitswap/client.py index 7858b3499..b801e48cd 100644 --- a/libp2p/bitswap/client.py +++ b/libp2p/bitswap/client.py @@ -35,7 +35,7 @@ BlockNotFoundError, BlockTooLargeError, MessageTooLargeError, - TimeoutError as BitswapTimeoutError, + BitswapTimeoutError, ) from .messages import create_message, create_wantlist_entry from .pb.bitswap_pb2 import Message diff --git a/libp2p/bitswap/errors.py b/libp2p/bitswap/errors.py index b4903964f..f88acf3b2 100644 --- a/libp2p/bitswap/errors.py +++ b/libp2p/bitswap/errors.py @@ -2,8 +2,12 @@ Bitswap protocol errors. """ +from libp2p.exceptions import ( + ProtocolError, +) -class BitswapError(Exception): + +class BitswapError(ProtocolError): """Base exception for Bitswap errors.""" pass @@ -27,8 +31,8 @@ class MessageTooLargeError(BitswapError): pass -class TimeoutError(BitswapError): - """Raised when an operation times out.""" +class BitswapTimeoutError(BitswapError): + """Raised when a Bitswap operation times out.""" pass diff --git a/libp2p/crypto/authenticated_encryption.py b/libp2p/crypto/authenticated_encryption.py index 70f15d45a..be5ab3a11 100644 --- a/libp2p/crypto/authenticated_encryption.py +++ b/libp2p/crypto/authenticated_encryption.py @@ -8,8 +8,13 @@ ) import Crypto.Util.Counter as Counter +from libp2p.crypto.exceptions import ( + CryptographyError, +) + -class InvalidMACException(Exception): +class InvalidMACException(CryptographyError): + """Raised when MAC validation fails.""" pass diff --git a/libp2p/discovery/rendezvous/errors.py b/libp2p/discovery/rendezvous/errors.py index e765d5398..2c2f457c0 100644 --- a/libp2p/discovery/rendezvous/errors.py +++ b/libp2p/discovery/rendezvous/errors.py @@ -2,10 +2,14 @@ Rendezvous protocol error handling. """ +from libp2p.exceptions import ( + DiscoveryError, +) + from .pb.rendezvous_pb2 import Message -class RendezvousError(Exception): +class RendezvousError(DiscoveryError): """Base exception for rendezvous protocol errors.""" def __init__(self, status: Message.ResponseStatus.ValueType, message: str = ""): diff --git a/libp2p/exceptions.py b/libp2p/exceptions.py index f4f80b5f8..1e4d535ec 100644 --- a/libp2p/exceptions.py +++ b/libp2p/exceptions.py @@ -4,7 +4,43 @@ class BaseLibp2pError(Exception): - pass + """ + Base exception for all libp2p errors. + + This exception serves as the root of the exception hierarchy, + allowing users to catch all libp2p-specific errors with a single + exception type. + + Example:: + >>> try: + ... # some libp2p operation + ... except BaseLibp2pError as e: + ... print(f"Libp2p error: {e}") + """ + + def __init__(self, message: str = "", *args, **kwargs): + """ + Initialize the exception. + + Args: + message: Error message + *args: Additional positional arguments + **kwargs: Additional keyword arguments (e.g., error_code, context) + """ + super().__init__(message, *args) + self.message = message + self.error_code = kwargs.get('error_code') + self.context = kwargs.get('context', {}) + + def __str__(self) -> str: + """Return string representation of the error.""" + if self.error_code: + return f"[{self.error_code}] {self.message}" + return self.message + + def __repr__(self) -> str: + """Return detailed representation of the error.""" + return f"{self.__class__.__name__}(message={self.message!r}, error_code={self.error_code!r})" class ValidationError(BaseLibp2pError): @@ -12,6 +48,79 @@ class ValidationError(BaseLibp2pError): class ParseError(BaseLibp2pError): + """Raised when parsing fails.""" + pass + + +# Intermediate base classes for logical grouping of exceptions + +class NetworkError(BaseLibp2pError): + """ + Base exception for network-related errors. + + This includes errors related to connections, streams, transport, + and network operations. + """ + pass + + +class ProtocolError(BaseLibp2pError): + """ + Base exception for protocol-related errors. + + This includes errors related to protocol negotiation, protocol + violations, and protocol-specific errors. + """ + pass + + +class PeerError(BaseLibp2pError): + """ + Base exception for peer-related errors. + + This includes errors related to peer store, peer data, peer + addresses, and peer serialization. + """ + pass + + +class ResourceError(BaseLibp2pError): + """ + Base exception for resource management errors. + + This includes errors related to resource limits, resource + allocation, and resource monitoring. + """ + pass + + +class ServiceError(BaseLibp2pError): + """ + Base exception for service lifecycle errors. + + This includes errors related to service management, service + lifecycle, and service operations. + """ + pass + + +class DiscoveryError(BaseLibp2pError): + """ + Base exception for discovery-related errors. + + This includes errors related to peer discovery, rendezvous, + and routing table operations. + """ + pass + + +class PubsubError(BaseLibp2pError): + """ + Base exception for pubsub-related errors. + + This includes errors related to pubsub routers, pubsub + operations, and pubsub protocols. + """ pass diff --git a/libp2p/network/exceptions.py b/libp2p/network/exceptions.py index 06849011f..2687c1689 100644 --- a/libp2p/network/exceptions.py +++ b/libp2p/network/exceptions.py @@ -1,7 +1,8 @@ from libp2p.exceptions import ( - BaseLibp2pError, + NetworkError, ) -class SwarmException(BaseLibp2pError): +class SwarmException(NetworkError): + """Exception raised by swarm operations.""" pass diff --git a/libp2p/peer/peerdata.py b/libp2p/peer/peerdata.py index 0d1a2f35b..ce4739417 100644 --- a/libp2p/peer/peerdata.py +++ b/libp2p/peer/peerdata.py @@ -17,6 +17,9 @@ PrivateKey, PublicKey, ) +from libp2p.exceptions import ( + PeerError, +) """ Latency EWMA Smoothing governs the deacy of the EWMA (the speed at which @@ -231,5 +234,11 @@ def is_expired(self) -> bool: return False -class PeerDataError(KeyError): - """Raised when a key is not found in peer metadata.""" +class PeerDataError(PeerError, KeyError): + """ + Raised when a key is not found in peer metadata. + + This exception uses multiple inheritance to maintain compatibility + with code that catches KeyError while also being part of the + libp2p exception hierarchy. + """ diff --git a/libp2p/peer/peerinfo.py b/libp2p/peer/peerinfo.py index 6601a4f1e..4a6c986b4 100644 --- a/libp2p/peer/peerinfo.py +++ b/libp2p/peer/peerinfo.py @@ -83,5 +83,17 @@ def peer_info_from_bytes(data: bytes) -> PeerInfo: raise InvalidAddrError(f"failed to decode PeerInfo: {e}") -class InvalidAddrError(ValueError): +from libp2p.exceptions import ( + PeerError, +) + + +class InvalidAddrError(PeerError, ValueError): + """ + Raised when an invalid address is encountered. + + This exception uses multiple inheritance to maintain compatibility + with code that catches ValueError while also being part of the + libp2p exception hierarchy. + """ pass diff --git a/libp2p/peer/peerstore.py b/libp2p/peer/peerstore.py index ddf1af1f2..96448aedf 100644 --- a/libp2p/peer/peerstore.py +++ b/libp2p/peer/peerstore.py @@ -24,6 +24,9 @@ PrivateKey, PublicKey, ) +from libp2p.exceptions import ( + PeerError, +) from libp2p.peer.envelope import Envelope, seal_record from libp2p.peer.peer_record import PeerRecord @@ -574,5 +577,11 @@ def clear_metrics(self, peer_id: ID) -> None: peer_data.clear_metrics() -class PeerStoreError(KeyError): - """Raised when peer ID is not found in peer store.""" +class PeerStoreError(PeerError, KeyError): + """ + Raised when peer ID is not found in peer store. + + This exception uses multiple inheritance to maintain compatibility + with code that catches KeyError while also being part of the + libp2p exception hierarchy. + """ diff --git a/libp2p/peer/persistent/serialization.py b/libp2p/peer/persistent/serialization.py index c998e5aa4..b3c84ebfc 100644 --- a/libp2p/peer/persistent/serialization.py +++ b/libp2p/peer/persistent/serialization.py @@ -33,7 +33,12 @@ logger = logging.getLogger(__name__) -class SerializationError(Exception): +from libp2p.exceptions import ( + PeerError, +) + + +class SerializationError(PeerError): """Raised when serialization or deserialization fails.""" diff --git a/libp2p/pubsub/exceptions.py b/libp2p/pubsub/exceptions.py index e203cad36..c12d292d4 100644 --- a/libp2p/pubsub/exceptions.py +++ b/libp2p/pubsub/exceptions.py @@ -1,9 +1,10 @@ from libp2p.exceptions import ( - BaseLibp2pError, + PubsubError, ) -class PubsubRouterError(BaseLibp2pError): +class PubsubRouterError(PubsubError): + """Exception raised by pubsub router operations.""" pass diff --git a/libp2p/rcmgr/circuit_breaker.py b/libp2p/rcmgr/circuit_breaker.py index 6877ed152..261e4f2e3 100644 --- a/libp2p/rcmgr/circuit_breaker.py +++ b/libp2p/rcmgr/circuit_breaker.py @@ -12,6 +12,10 @@ import time from typing import Any +from libp2p.exceptions import ( + ResourceError, +) + class CircuitBreakerState(Enum): """Circuit breaker states.""" @@ -21,7 +25,7 @@ class CircuitBreakerState(Enum): HALF_OPEN = "half_open" -class CircuitBreakerError(Exception): +class CircuitBreakerError(ResourceError): """Exception raised when circuit breaker is open.""" pass diff --git a/libp2p/rcmgr/enhanced_errors.py b/libp2p/rcmgr/enhanced_errors.py index fc10d0045..2b9c9d672 100644 --- a/libp2p/rcmgr/enhanced_errors.py +++ b/libp2p/rcmgr/enhanced_errors.py @@ -15,6 +15,9 @@ import multiaddr +from libp2p.exceptions import ( + ResourceError, +) from libp2p.peer.id import ID @@ -192,7 +195,7 @@ def __str__(self) -> str: ) -class ResourceLimitExceededError(Exception): +class ResourceLimitExceededError(ResourceError): """Enhanced exception for resource limit exceeded errors.""" def __init__( @@ -266,7 +269,7 @@ def __str__(self) -> str: return self._build_error_message() -class SystemResourceError(Exception): +class SystemResourceError(ResourceError): """Enhanced exception for system resource errors.""" def __init__( @@ -332,7 +335,7 @@ def __str__(self) -> str: return self._build_error_message() -class ConfigurationError(Exception): +class ConfigurationError(ResourceError): """Enhanced exception for configuration errors.""" def __init__( @@ -389,7 +392,7 @@ def __str__(self) -> str: return self._build_error_message() -class OperationalError(Exception): +class OperationalError(ResourceError): """Enhanced exception for operational errors.""" def __init__( diff --git a/libp2p/rcmgr/exceptions.py b/libp2p/rcmgr/exceptions.py index 00de25e12..41d055e59 100644 --- a/libp2p/rcmgr/exceptions.py +++ b/libp2p/rcmgr/exceptions.py @@ -4,8 +4,12 @@ from __future__ import annotations +from libp2p.exceptions import ( + ResourceError, +) -class ResourceManagerException(Exception): + +class ResourceManagerException(ResourceError): """Base exception for all resource manager errors.""" def __init__(self, message: str): diff --git a/libp2p/transport/quic/exceptions.py b/libp2p/transport/quic/exceptions.py index 2df3dda5c..49975b17d 100644 --- a/libp2p/transport/quic/exceptions.py +++ b/libp2p/transport/quic/exceptions.py @@ -4,12 +4,16 @@ from typing import Any, Literal +from libp2p.exceptions import ( + NetworkError, +) -class QUICError(Exception): + +class QUICError(NetworkError): """Base exception for all QUIC transport errors.""" def __init__(self, message: str, error_code: int | None = None): - super().__init__(message) + super().__init__(message, error_code=error_code) self.error_code = error_code diff --git a/tests/core/bitswap/test_client.py b/tests/core/bitswap/test_client.py index fc8665810..4b125085a 100644 --- a/tests/core/bitswap/test_client.py +++ b/tests/core/bitswap/test_client.py @@ -11,7 +11,7 @@ BITSWAP_PROTOCOL_V100, BITSWAP_PROTOCOL_V120, ) -from libp2p.bitswap.errors import TimeoutError as BitswapTimeoutError +from libp2p.bitswap.errors import BitswapTimeoutError from libp2p.peer.id import ID as PeerID diff --git a/tests/core/exceptions/test_exceptions.py b/tests/core/exceptions/test_exceptions.py index f60cabe36..b366aec8d 100644 --- a/tests/core/exceptions/test_exceptions.py +++ b/tests/core/exceptions/test_exceptions.py @@ -1,4 +1,12 @@ from libp2p.exceptions import ( + BaseLibp2pError, + DiscoveryError, + NetworkError, + PeerError, + ProtocolError, + PubsubError, + ResourceError, + ServiceError, MultiError, ) @@ -15,3 +23,200 @@ def test_multierror_str_and_storage(): # Check for representation expected = "Error 1: bad value\nError 2: 'missing key'\nError 3: custom error" assert str(multi_error) == expected + + +def test_base_libp2p_error_inheritance(): + """Test that BaseLibp2pError is properly set up.""" + error = BaseLibp2pError("test message") + assert isinstance(error, Exception) + assert error.message == "test message" + assert str(error) == "test message" + + +def test_base_libp2p_error_with_error_code(): + """Test BaseLibp2pError with error code.""" + error = BaseLibp2pError("test message", error_code="ERR001") + assert error.error_code == "ERR001" + assert str(error) == "[ERR001] test message" + + +def test_quic_error_inheritance(): + """Test that QUIC exceptions inherit from BaseLibp2pError.""" + from libp2p.transport.quic.exceptions import ( + QUICError, + QUICConnectionError, + QUICStreamError, + ) + + assert issubclass(QUICError, NetworkError) + assert issubclass(QUICError, BaseLibp2pError) + assert issubclass(QUICConnectionError, QUICError) + assert issubclass(QUICStreamError, QUICError) + + # Test instance + error = QUICError("test", error_code=1) + assert isinstance(error, BaseLibp2pError) + assert isinstance(error, NetworkError) + + +def test_bitswap_error_inheritance(): + """Test that Bitswap exceptions inherit from BaseLibp2pError.""" + from libp2p.bitswap.errors import ( + BitswapError, + BitswapTimeoutError, + InvalidBlockError, + ) + + assert issubclass(BitswapError, ProtocolError) + assert issubclass(BitswapError, BaseLibp2pError) + assert issubclass(BitswapTimeoutError, BitswapError) + assert issubclass(InvalidBlockError, BitswapError) + + # Test instance + error = BitswapError("test") + assert isinstance(error, BaseLibp2pError) + assert isinstance(error, ProtocolError) + + +def test_rendezvous_error_inheritance(): + """Test that Rendezvous exceptions inherit from BaseLibp2pError.""" + from libp2p.discovery.rendezvous.errors import ( + RendezvousError, + InvalidNamespaceError, + ) + + assert issubclass(RendezvousError, DiscoveryError) + assert issubclass(RendezvousError, BaseLibp2pError) + assert issubclass(InvalidNamespaceError, RendezvousError) + + # Test instance + from libp2p.discovery.rendezvous.errors import Message + error = RendezvousError(Message.ResponseStatus.E_INVALID_NAMESPACE, "test") + assert isinstance(error, BaseLibp2pError) + assert isinstance(error, DiscoveryError) + + +def test_peer_error_inheritance(): + """Test that peer exceptions inherit from BaseLibp2pError.""" + from libp2p.peer.peerstore import PeerStoreError + from libp2p.peer.peerdata import PeerDataError + from libp2p.peer.peerinfo import InvalidAddrError + from libp2p.peer.persistent.serialization import SerializationError + + # Test inheritance + assert issubclass(PeerStoreError, PeerError) + assert issubclass(PeerStoreError, BaseLibp2pError) + assert issubclass(PeerDataError, PeerError) + assert issubclass(PeerDataError, BaseLibp2pError) + assert issubclass(InvalidAddrError, PeerError) + assert issubclass(InvalidAddrError, BaseLibp2pError) + assert issubclass(SerializationError, PeerError) + assert issubclass(SerializationError, BaseLibp2pError) + + # Test multiple inheritance for backwards compatibility + assert issubclass(PeerStoreError, KeyError) + assert issubclass(PeerDataError, KeyError) + assert issubclass(InvalidAddrError, ValueError) + + # Test instances + error = PeerStoreError("test") + assert isinstance(error, BaseLibp2pError) + assert isinstance(error, PeerError) + assert isinstance(error, KeyError) + + +def test_resource_error_inheritance(): + """Test that resource manager exceptions inherit from BaseLibp2pError.""" + from libp2p.rcmgr.exceptions import ( + ResourceManagerException, + ResourceLimitExceeded, + ) + from libp2p.rcmgr.circuit_breaker import CircuitBreakerError + from libp2p.rcmgr.enhanced_errors import ( + ResourceLimitExceededError, + SystemResourceError, + ) + + assert issubclass(ResourceManagerException, ResourceError) + assert issubclass(ResourceManagerException, BaseLibp2pError) + assert issubclass(ResourceLimitExceeded, ResourceManagerException) + assert issubclass(CircuitBreakerError, ResourceError) + assert issubclass(ResourceLimitExceededError, ResourceError) + assert issubclass(SystemResourceError, ResourceError) + + # Test instances + error = ResourceManagerException("test") + assert isinstance(error, BaseLibp2pError) + assert isinstance(error, ResourceError) + + +def test_network_error_inheritance(): + """Test that network exceptions inherit from BaseLibp2pError.""" + from libp2p.network.exceptions import SwarmException + from libp2p.network.stream.exceptions import StreamError + + assert issubclass(SwarmException, NetworkError) + assert issubclass(SwarmException, BaseLibp2pError) + assert issubclass(StreamError, BaseLibp2pError) # StreamError inherits from IOException + + # Test instances + error = SwarmException("test") + assert isinstance(error, BaseLibp2pError) + assert isinstance(error, NetworkError) + + +def test_pubsub_error_inheritance(): + """Test that pubsub exceptions inherit from BaseLibp2pError.""" + from libp2p.pubsub.exceptions import ( + PubsubRouterError, + NoPubsubAttached, + ) + + assert issubclass(PubsubRouterError, PubsubError) + assert issubclass(PubsubRouterError, BaseLibp2pError) + assert issubclass(NoPubsubAttached, PubsubRouterError) + + # Test instances + error = PubsubRouterError("test") + assert isinstance(error, BaseLibp2pError) + assert isinstance(error, PubsubError) + + +def test_crypto_error_inheritance(): + """Test that crypto exceptions inherit from BaseLibp2pError.""" + from libp2p.crypto.exceptions import CryptographyError + from libp2p.crypto.authenticated_encryption import InvalidMACException + + assert issubclass(CryptographyError, BaseLibp2pError) + assert issubclass(InvalidMACException, CryptographyError) + assert issubclass(InvalidMACException, BaseLibp2pError) + + # Test instances + error = InvalidMACException("test") + assert isinstance(error, BaseLibp2pError) + assert isinstance(error, CryptographyError) + + +def test_catch_all_libp2p_errors(): + """Test that we can catch all libp2p errors with BaseLibp2pError.""" + from libp2p.transport.quic.exceptions import QUICError + from libp2p.bitswap.errors import BitswapError + from libp2p.network.exceptions import SwarmException + from libp2p.rcmgr.exceptions import ResourceManagerException + + exceptions = [ + QUICError("quic error"), + BitswapError("bitswap error"), + SwarmException("swarm error"), + ResourceManagerException("resource error"), + ] + + for exc in exceptions: + try: + raise exc + except BaseLibp2pError: + # Should catch all libp2p errors + pass + except Exception: + # Should not reach here + assert False, f"{exc} should be caught by BaseLibp2pError"