Skip to content

Commit 52f0c6a

Browse files
feat: add __repr__ to client exception classes for debugging parity with server errors (#683)
# Description Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Follow the [`CONTRIBUTING` Guide](https://github.com/a2aproject/a2a-python/blob/main/CONTRIBUTING.md). - [x] Make your Pull Request title in the <https://www.conventionalcommits.org/> specification. - Important Prefixes for [release-please](https://github.com/googleapis/release-please): - `fix:` which represents bug fixes, and correlates to a [SemVer](https://semver.org/) patch. - `feat:` represents a new feature, and correlates to a SemVer minor. - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. - [x] Ensure the tests and linter pass (Run `bash scripts/format.sh` from the repository root to format) - [x] Appropriate docs were updated (if necessary) Fixes #679 🦕 ## Problem Server-side exceptions (`ServerError`) implement `__repr__` for developer-friendly debugging output, but client-side exceptions (`A2AClientHTTPError`, `A2AClientJSONError`, `A2AClientTimeoutError`, `A2AClientInvalidArgsError`, `A2AClientInvalidStateError`, `A2AClientJSONRPCError`) do not. When these exceptions appear during debugging, they show minimal information via the default `Exception.__repr__`, which only wraps `str()` output and loses structured attributes: ```python >>> repr(A2AClientHTTPError(404, 'Not Found')) "A2AClientHTTPError('HTTP Error 404: Not Found')" # Loses status_code as a distinct attribute ```` For `A2AClientJSONRPCError`, the situation is worse — `repr` flattens the underlying `JSONRPCError` object into a single string, losing structured access to the error code and data during inspection. ## Fix Added `__repr__` methods to all client exception classes in `src/a2a/client/errors.py`, following the same pattern already established by `ServerError.__repr__` in `src/a2a/utils/errors.py`: ```python >>> repr(A2AClientHTTPError(404, 'Not Found')) "A2AClientHTTPError(status_code=404, message='Not Found')" ```` `A2AClientJSONError`, `A2AClientTimeoutError`, `A2AClientInvalidArgsError`, `A2AClientInvalidStateError`: Expose message as a `keyword` argument. ```python >>> repr(A2AClientTimeoutError('Connection timed out after 30s')) "A2AClientTimeoutError(message='Connection timed out after 30s')" ``` `A2AClientJSONRPCError`: Delegates to `repr()` of the inner `JSONRPCError` object, preserving structured error code and data. ```python >>> repr(A2AClientJSONRPCError(response)) "A2AClientJSONRPCError(JSONRPCError(code=-32600, message='Invalid Request', data=None))" ``` This change does not alter traceback formatting, `__str__` output, exception hierarchy, catching behavior, or any existing error handling logic. It is purely additive and intended to improve debuggability via `repr()` in REPLs, logs, and structured logging contexts. ## Test Tests were added directly into the existing suite at `tests/client/test_errors.py`, keeping the same structure and conventions already in place. No existing test was modified or removed — all additions are purely additive. Happy to adapt the test approach based on maintainer feedback. Release-As: 0.3.23 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 63c1e93 commit 52f0c6a

2 files changed

Lines changed: 101 additions & 0 deletions

File tree

src/a2a/client/errors.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ def __init__(self, status_code: int, message: str):
2121
self.message = message
2222
super().__init__(f'HTTP Error {status_code}: {message}')
2323

24+
def __repr__(self) -> str:
25+
"""Returns an unambiguous representation showing structured attributes."""
26+
return (
27+
f'{self.__class__.__name__}('
28+
f'status_code={self.status_code!r}, '
29+
f'message={self.message!r})'
30+
)
31+
2432

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

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

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

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

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

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

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

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

77101
class A2AClientJSONRPCError(A2AClientError):
78102
"""Client exception for JSON-RPC errors returned by the server."""
@@ -85,3 +109,7 @@ def __init__(self, error: JSONRPCErrorResponse):
85109
"""
86110
self.error = error.error
87111
super().__init__(f'JSON-RPC Error {error.error}')
112+
113+
def __repr__(self) -> str:
114+
"""Returns an unambiguous representation showing the JSON-RPC error object."""
115+
return f'{self.__class__.__name__}({self.error!r})'

tests/client/test_errors.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
import pytest
44

55
from a2a.client import A2AClientError, A2AClientHTTPError, A2AClientJSONError
6+
from a2a.client.errors import (
7+
A2AClientInvalidArgsError,
8+
A2AClientInvalidStateError,
9+
A2AClientJSONRPCError,
10+
A2AClientTimeoutError,
11+
)
12+
from a2a.types import JSONRPCError, JSONRPCErrorResponse
613

714

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

45+
def test_repr(self) -> None:
46+
"""Test that __repr__ shows structured attributes."""
47+
error = A2AClientHTTPError(404, 'Not Found')
48+
assert (
49+
repr(error)
50+
== "A2AClientHTTPError(status_code=404, message='Not Found')"
51+
)
52+
3853
def test_inheritance(self) -> None:
3954
"""Test that A2AClientHTTPError inherits from A2AClientError."""
4055
error = A2AClientHTTPError(400, 'Bad Request')
@@ -81,6 +96,13 @@ def test_message_formatting(self) -> None:
8196
error = A2AClientJSONError('Missing required field')
8297
assert str(error) == 'JSON Error: Missing required field'
8398

99+
def test_repr(self) -> None:
100+
"""Test that __repr__ shows structured attributes."""
101+
error = A2AClientJSONError('Invalid JSON format')
102+
assert (
103+
repr(error) == "A2AClientJSONError(message='Invalid JSON format')"
104+
)
105+
84106
def test_inheritance(self) -> None:
85107
"""Test that A2AClientJSONError inherits from A2AClientError."""
86108
error = A2AClientJSONError('Parsing error')
@@ -108,6 +130,57 @@ def test_with_various_messages(self) -> None:
108130
assert str(error) == f'JSON Error: {message}'
109131

110132

133+
class TestA2AClientTimeoutErrorRepr:
134+
"""Test __repr__ for A2AClientTimeoutError."""
135+
136+
def test_repr(self) -> None:
137+
"""Test that __repr__ shows structured attributes."""
138+
error = A2AClientTimeoutError('Request timed out')
139+
assert (
140+
repr(error) == "A2AClientTimeoutError(message='Request timed out')"
141+
)
142+
143+
144+
class TestA2AClientInvalidArgsErrorRepr:
145+
"""Test __repr__ for A2AClientInvalidArgsError."""
146+
147+
def test_repr(self) -> None:
148+
"""Test that __repr__ shows structured attributes."""
149+
error = A2AClientInvalidArgsError('Missing required param')
150+
assert (
151+
repr(error)
152+
== "A2AClientInvalidArgsError(message='Missing required param')"
153+
)
154+
155+
156+
class TestA2AClientInvalidStateErrorRepr:
157+
"""Test __repr__ for A2AClientInvalidStateError."""
158+
159+
def test_repr(self) -> None:
160+
"""Test that __repr__ shows structured attributes."""
161+
error = A2AClientInvalidStateError('Client not initialized')
162+
assert (
163+
repr(error)
164+
== "A2AClientInvalidStateError(message='Client not initialized')"
165+
)
166+
167+
168+
class TestA2AClientJSONRPCErrorRepr:
169+
"""Test __repr__ for A2AClientJSONRPCError."""
170+
171+
def test_repr(self) -> None:
172+
"""Test that __repr__ shows the JSON-RPC error object."""
173+
response = JSONRPCErrorResponse(
174+
id='test-1',
175+
error=JSONRPCError(code=-32601, message='Method not found'),
176+
)
177+
error = A2AClientJSONRPCError(response)
178+
assert (
179+
repr(error)
180+
== "A2AClientJSONRPCError(JSONRPCError(code=-32601, data=None, message='Method not found'))"
181+
)
182+
183+
111184
class TestExceptionHierarchy:
112185
"""Test the exception hierarchy and relationships."""
113186

0 commit comments

Comments
 (0)