diff --git a/kasa/cli/common.py b/kasa/cli/common.py index 649df065..5114f7af 100644 --- a/kasa/cli/common.py +++ b/kasa/cli/common.py @@ -2,12 +2,14 @@ from __future__ import annotations +import asyncio import json import re import sys from collections.abc import Callable from contextlib import contextmanager from functools import singledispatch, update_wrapper, wraps +from gettext import gettext from typing import TYPE_CHECKING, Any, Final import asyncclick as click @@ -238,4 +240,19 @@ def CatchAllExceptions(cls): except Exception as exc: _handle_exception(self._debug, exc) + def __call__(self, *args, **kwargs): + """Run the coroutine in the event loop and print any exceptions. + + python click catches KeyboardInterrupt in main, raises Abort() + and does sys.exit. asyncclick doesn't properly handle a coroutine + receiving CancelledError on a KeyboardInterrupt, so we catch the + KeyboardInterrupt here once asyncio.run has re-raised it. This + avoids large stacktraces when a user presses Ctrl-C. + """ + try: + asyncio.run(self.main(*args, **kwargs)) + except KeyboardInterrupt: + click.echo(gettext("\nAborted!"), file=sys.stderr) + sys.exit(1) + return _CommandCls diff --git a/kasa/discover.py b/kasa/discover.py index 77ef80be..b696c370 100755 --- a/kasa/discover.py +++ b/kasa/discover.py @@ -498,7 +498,7 @@ class Discover: try: _LOGGER.debug("Waiting %s seconds for responses...", discovery_timeout) await protocol.wait_for_discovery_to_complete() - except KasaException as ex: + except (KasaException, asyncio.CancelledError) as ex: for device in protocol.discovered_devices.values(): await device.protocol.close() raise ex