Skip to content
Merged
28 changes: 28 additions & 0 deletions src/a2a/client/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ def __init__(self, status_code: int, message: str):
self.message = message
super().__init__(f'HTTP Error {status_code}: {message}')

def __repr__(self) -> str:
"""Returns an unambiguous representation showing structured attributes."""
return (
f'{self.__class__.__name__}('
f'status_code={self.status_code!r}, '
f'message={self.message!r})'
)


class A2AClientJSONError(A2AClientError):
"""Client exception for JSON errors during response parsing or validation."""
Expand All @@ -34,6 +42,10 @@ def __init__(self, message: str):
self.message = message
super().__init__(f'JSON Error: {message}')

def __repr__(self) -> str:
"""Returns an unambiguous representation showing structured attributes."""
return f'{self.__class__.__name__}(message={self.message!r})'
Comment thread
ishymko marked this conversation as resolved.


class A2AClientTimeoutError(A2AClientError):
"""Client exception for timeout errors during a request."""
Expand All @@ -47,6 +59,10 @@ def __init__(self, message: str):
self.message = message
super().__init__(f'Timeout Error: {message}')

def __repr__(self) -> str:
"""Returns an unambiguous representation showing structured attributes."""
return f'{self.__class__.__name__}(message={self.message!r})'


class A2AClientInvalidArgsError(A2AClientError):
"""Client exception for invalid arguments passed to a method."""
Expand All @@ -60,6 +76,10 @@ def __init__(self, message: str):
self.message = message
super().__init__(f'Invalid arguments error: {message}')

def __repr__(self) -> str:
"""Returns an unambiguous representation showing structured attributes."""
return f'{self.__class__.__name__}(message={self.message!r})'


class A2AClientInvalidStateError(A2AClientError):
"""Client exception for an invalid client state."""
Expand All @@ -73,6 +93,10 @@ def __init__(self, message: str):
self.message = message
super().__init__(f'Invalid state error: {message}')

def __repr__(self) -> str:
"""Returns an unambiguous representation showing structured attributes."""
return f'{self.__class__.__name__}(message={self.message!r})'


class A2AClientJSONRPCError(A2AClientError):
"""Client exception for JSON-RPC errors returned by the server."""
Expand All @@ -85,3 +109,7 @@ def __init__(self, error: JSONRPCErrorResponse):
"""
self.error = error.error
super().__init__(f'JSON-RPC Error {error.error}')

def __repr__(self) -> str:
"""Returns an unambiguous representation showing the JSON-RPC error object."""
return f'{self.__class__.__name__}({self.error!r})'
195 changes: 154 additions & 41 deletions tests/client/test_errors.py
Comment thread
ishymko marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from typing import NoReturn
from unittest.mock import MagicMock

import pytest

from a2a.client import A2AClientError, A2AClientHTTPError, A2AClientJSONError
from a2a.client.errors import (
A2AClientInvalidArgsError,
A2AClientInvalidStateError,
A2AClientJSONRPCError,
A2AClientTimeoutError,
)


class TestA2AClientError:
Expand Down Expand Up @@ -35,6 +42,14 @@ def test_message_formatting(self) -> None:
error = A2AClientHTTPError(500, 'Internal Server Error')
assert str(error) == 'HTTP Error 500: Internal Server Error'

def test_repr(self) -> None:
"""Test that __repr__ shows structured attributes."""
error = A2AClientHTTPError(404, 'Not Found')
assert (
repr(error)
== "A2AClientHTTPError(status_code=404, message='Not Found')"
)

def test_inheritance(self) -> None:
"""Test that A2AClientHTTPError inherits from A2AClientError."""
error = A2AClientHTTPError(400, 'Bad Request')
Expand Down Expand Up @@ -67,45 +82,96 @@ def test_with_various_status_codes(self) -> None:
assert str(error) == f'HTTP Error {status_code}: {message}'


class TestA2AClientJSONError:
"""Test cases for A2AClientJSONError class."""
@pytest.mark.parametrize(
'error_class, prefix, name',
[
(A2AClientJSONError, 'JSON Error', 'A2AClientJSONError'),
(A2AClientTimeoutError, 'Timeout Error', 'A2AClientTimeoutError'),
(
A2AClientInvalidArgsError,
'Invalid arguments error',
'A2AClientInvalidArgsError',
),
(
A2AClientInvalidStateError,
'Invalid state error',
'A2AClientInvalidStateError',
),
],
)
class TestSingleMessageClientError:
"""Test cases for client errors that take a single message argument."""

def test_instantiation(self) -> None:
"""Test that A2AClientJSONError can be instantiated with a message."""
error = A2AClientJSONError('Invalid JSON format')
def test_instantiation(self, error_class, prefix, name) -> None:
"""Test that the error can be instantiated with a message."""
error = error_class('Test message')
assert isinstance(error, A2AClientError)
assert error.message == 'Invalid JSON format'
assert error.message == 'Test message'

def test_message_formatting(self) -> None:
def test_message_formatting(self, error_class, prefix, name) -> None:
"""Test that the error message is formatted correctly."""
error = A2AClientJSONError('Missing required field')
assert str(error) == 'JSON Error: Missing required field'
error = error_class('Details here')
assert str(error) == f'{prefix}: Details here'

def test_inheritance(self) -> None:
"""Test that A2AClientJSONError inherits from A2AClientError."""
error = A2AClientJSONError('Parsing error')
def test_repr(self, error_class, prefix, name) -> None:
"""Test that __repr__ shows structured attributes."""
error = error_class('A test message')
assert repr(error) == f"{name}(message='A test message')"

def test_inheritance(self, error_class, prefix, name) -> None:
"""Test that the error inherits from A2AClientError."""
error = error_class('some message')
assert isinstance(error, A2AClientError)

def test_with_empty_message(self) -> None:
def test_with_empty_message(self, error_class, prefix, name) -> None:
"""Test behavior with an empty message."""
error = A2AClientJSONError('')
error = error_class('')
assert error.message == ''
assert str(error) == 'JSON Error: '

def test_with_various_messages(self) -> None:
"""Test with different error messages."""
test_messages = [
'Malformed JSON',
'Missing required fields',
'Invalid data type',
'Unexpected JSON structure',
'Empty JSON object',
]
assert str(error) == f'{prefix}: '

for message in test_messages:
error = A2AClientJSONError(message)
assert error.message == message
assert str(error) == f'JSON Error: {message}'

class TestA2AClientJSONRPCError:
"""Test cases for A2AClientJSONRPCError class."""

def _make_error_response(
self, code: int = -32600, message: str = 'Invalid Request', data=None
):
"""Helper to create a mock JSONRPCErrorResponse."""
inner_error = MagicMock()
inner_error.code = code
inner_error.message = message
inner_error.data = data

response = MagicMock()
response.error = inner_error
return response

def test_instantiation(self) -> None:
"""Test that A2AClientJSONRPCError can be instantiated."""
response = self._make_error_response()
error = A2AClientJSONRPCError(response)
assert isinstance(error, A2AClientError)
assert error.error == response.error

def test_repr(self) -> None:
"""Test that __repr__ shows the JSON-RPC error object."""
response = self._make_error_response(-32601, 'Method not found')
error = A2AClientJSONRPCError(response)
result = repr(error)
assert result == f'A2AClientJSONRPCError({response.error!r})'

def test_inheritance(self) -> None:
"""Test that A2AClientJSONRPCError inherits from A2AClientError."""
response = self._make_error_response()
error = A2AClientJSONRPCError(response)
assert isinstance(error, A2AClientError)

def test_with_empty_message(self) -> None:
"""Test behavior with an empty message."""
response = self._make_error_response(message='')
error = A2AClientJSONRPCError(response)
Comment thread
ishymko marked this conversation as resolved.
assert error.error.message == ''
assert str(error) == f'JSON-RPC Error {response.error}'


class TestExceptionHierarchy:
Expand All @@ -116,6 +182,10 @@ def test_exception_hierarchy(self) -> None:
assert issubclass(A2AClientError, Exception)
assert issubclass(A2AClientHTTPError, A2AClientError)
assert issubclass(A2AClientJSONError, A2AClientError)
assert issubclass(A2AClientTimeoutError, A2AClientError)
assert issubclass(A2AClientInvalidArgsError, A2AClientError)
assert issubclass(A2AClientInvalidStateError, A2AClientError)
assert issubclass(A2AClientJSONRPCError, A2AClientError)

def test_catch_specific_exception(self) -> None:
"""Test that specific exceptions can be caught."""
Expand All @@ -130,6 +200,9 @@ def test_catch_base_exception(self) -> None:
exceptions = [
A2AClientHTTPError(404, 'Not Found'),
A2AClientJSONError('Invalid JSON'),
A2AClientTimeoutError('Timed out'),
A2AClientInvalidArgsError('Bad args'),
A2AClientInvalidStateError('Bad state'),
]

for raised_error in exceptions:
Expand Down Expand Up @@ -168,38 +241,78 @@ def test_raising_base_error(self) -> NoReturn:

assert str(excinfo.value) == 'Generic client error'

def test_raising_timeout_error(self) -> NoReturn:
"""Test raising a timeout error and checking its properties."""
with pytest.raises(A2AClientTimeoutError) as excinfo:
raise A2AClientTimeoutError('Connection timed out')

error = excinfo.value
assert error.message == 'Connection timed out'
assert str(error) == 'Timeout Error: Connection timed out'


# Additional parametrized tests for more comprehensive coverage


@pytest.mark.parametrize(
'status_code,message,expected',
'status_code,message,expected_str,expected_repr',
[
(400, 'Bad Request', 'HTTP Error 400: Bad Request'),
(404, 'Not Found', 'HTTP Error 404: Not Found'),
(500, 'Server Error', 'HTTP Error 500: Server Error'),
(
400,
'Bad Request',
'HTTP Error 400: Bad Request',
"A2AClientHTTPError(status_code=400, message='Bad Request')",
),
(
404,
'Not Found',
'HTTP Error 404: Not Found',
"A2AClientHTTPError(status_code=404, message='Not Found')",
),
(
500,
'Server Error',
'HTTP Error 500: Server Error',
"A2AClientHTTPError(status_code=500, message='Server Error')",
),
],
)
def test_http_error_parametrized(
status_code: int, message: str, expected: str
status_code: int, message: str, expected_str: str, expected_repr: str
) -> None:
"""Parametrized test for HTTP errors with different status codes."""
error = A2AClientHTTPError(status_code, message)
assert error.status_code == status_code
assert error.message == message
assert str(error) == expected
assert str(error) == expected_str
assert repr(error) == expected_repr


@pytest.mark.parametrize(
'message,expected',
'message,expected_str,expected_repr',
[
('Missing field', 'JSON Error: Missing field'),
('Invalid type', 'JSON Error: Invalid type'),
('Parsing failed', 'JSON Error: Parsing failed'),
(
'Missing field',
'JSON Error: Missing field',
"A2AClientJSONError(message='Missing field')",
),
(
'Invalid type',
'JSON Error: Invalid type',
"A2AClientJSONError(message='Invalid type')",
),
(
'Parsing failed',
'JSON Error: Parsing failed',
"A2AClientJSONError(message='Parsing failed')",
),
],
)
def test_json_error_parametrized(message: str, expected: str) -> None:
def test_json_error_parametrized(
message: str, expected_str: str, expected_repr: str
) -> None:
"""Parametrized test for JSON errors with different messages."""
error = A2AClientJSONError(message)
assert error.message == message
assert str(error) == expected
assert str(error) == expected_str
assert repr(error) == expected_repr
Loading