Skip to content

Commit d66caa9

Browse files
committed
Have CopilotClient.stop() raise an exception group instead of returning a list of exceptions
The Zen of Python says, "Errors should never pass silently".
1 parent d87af41 commit d66caa9

File tree

4 files changed

+27
-21
lines changed

4 files changed

+27
-21
lines changed

python/copilot/client.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ async def start(self) -> None:
315315
) from e
316316
raise
317317

318-
async def stop(self) -> list["StopError"]:
318+
async def stop(self) -> None:
319319
"""
320320
Stop the CLI server and close all active sessions.
321321
@@ -324,14 +324,14 @@ async def stop(self) -> list["StopError"]:
324324
2. Closes the JSON-RPC connection
325325
3. Terminates the CLI server process (if spawned by this client)
326326
327-
Returns:
328-
A list of StopError objects containing error messages that occurred
329-
during cleanup. An empty list indicates all cleanup succeeded.
327+
Raises:
328+
ExceptionGroup[StopError]: If any errors occurred during cleanup.
330329
331330
Example:
332-
>>> errors = await client.stop()
333-
>>> if errors:
334-
... for error in errors:
331+
>>> try:
332+
... await client.stop()
333+
... except* StopError as eg:
334+
... for error in eg.exceptions:
335335
... print(f"Cleanup error: {error.message}")
336336
"""
337337
errors: list[StopError] = []
@@ -360,7 +360,6 @@ async def stop(self) -> list["StopError"]:
360360
async with self._models_cache_lock:
361361
self._models_cache = None
362362

363-
# Kill CLI process
364363
# Kill CLI process (only if we spawned it)
365364
if self._process and not self._is_external_server:
366365
self._process.terminate()
@@ -374,7 +373,8 @@ async def stop(self) -> list["StopError"]:
374373
if not self._is_external_server:
375374
self._actual_port = None
376375

377-
return errors
376+
if errors:
377+
raise ExceptionGroup("errors during CopilotClient.stop()", errors)
378378

379379
async def force_stop(self) -> None:
380380
"""

python/copilot/types.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,11 +619,14 @@ def to_dict(self) -> dict:
619619

620620
# Error information from client stop
621621
@dataclass
622-
class StopError:
623-
"""Error information from client stop"""
622+
class StopError(Exception):
623+
"""Error that occurred during client stop cleanup."""
624624

625625
message: str # Error message describing what failed during cleanup
626626

627+
def __post_init__(self) -> None:
628+
Exception.__init__(self, self.message)
629+
627630
@staticmethod
628631
def from_dict(obj: Any) -> StopError:
629632
assert isinstance(obj, dict)

python/e2e/test_client.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from copilot import CopilotClient, PermissionHandler
5+
from copilot import CopilotClient, PermissionHandler, StopError
66

77
from .testharness import CLI_PATH
88

@@ -20,8 +20,7 @@ async def test_should_start_and_connect_to_server_using_stdio(self):
2020
assert pong.message == "pong: test message"
2121
assert pong.timestamp >= 0
2222

23-
errors = await client.stop()
24-
assert len(errors) == 0
23+
await client.stop()
2524
assert client.get_state() == "disconnected"
2625
finally:
2726
await client.force_stop()
@@ -38,14 +37,13 @@ async def test_should_start_and_connect_to_server_using_tcp(self):
3837
assert pong.message == "pong: test message"
3938
assert pong.timestamp >= 0
4039

41-
errors = await client.stop()
42-
assert len(errors) == 0
40+
await client.stop()
4341
assert client.get_state() == "disconnected"
4442
finally:
4543
await client.force_stop()
4644

4745
@pytest.mark.asyncio
48-
async def test_should_return_errors_on_failed_cleanup(self):
46+
async def test_should_raise_exception_group_on_failed_cleanup(self):
4947
import asyncio
5048

5149
client = CopilotClient({"cli_path": CLI_PATH})
@@ -59,9 +57,11 @@ async def test_should_return_errors_on_failed_cleanup(self):
5957
process.kill()
6058
await asyncio.sleep(0.1)
6159

62-
errors = await client.stop()
63-
assert len(errors) > 0
64-
assert "Failed to destroy session" in errors[0].message
60+
with pytest.raises(ExceptionGroup) as exc_info:
61+
await client.stop()
62+
assert len(exc_info.value.exceptions) > 0
63+
assert isinstance(exc_info.value.exceptions[0], StopError)
64+
assert "Failed to destroy session" in exc_info.value.exceptions[0].message
6565
finally:
6666
await client.force_stop()
6767

python/e2e/testharness/context.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ async def teardown(self, test_failed: bool = False):
7777
test_failed: If True, skip writing snapshots to avoid corruption.
7878
"""
7979
if self._client:
80-
await self._client.stop()
80+
try:
81+
await self._client.stop()
82+
except ExceptionGroup:
83+
pass # stop() completes all cleanup before raising; safe to ignore in teardown
8184
self._client = None
8285

8386
if self._proxy:

0 commit comments

Comments
 (0)