From d75df76e323df87a3a95ed04e395dd91932b9d0a Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Pelayo Date: Sat, 8 Nov 2025 22:12:14 +0100 Subject: [PATCH] j: better error if JUMPSTARTER_HOST isn't set (#739) * j: better error if JUMPSTARTER_HOST isn't set until now if j is called outside of a jmp shell, a ugly exception is thrown, this patch produces an error explaining what's going on. * Raise exceptions not in group Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> (cherry picked from commit 44027366c7a8465cf30d017845f8fcd1251eff4f) --- .../jumpstarter_cli_common/exceptions.py | 20 ++++++++++++++++ packages/jumpstarter-cli/jumpstarter_cli/j.py | 24 +++++++++++++------ .../jumpstarter/common/exceptions.py | 6 +++++ packages/jumpstarter/jumpstarter/utils/env.py | 3 ++- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py index dd989cc88..7595d9462 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py @@ -106,6 +106,26 @@ def wrapped(*args, **kwargs): return decorator +def find_exception_in_group( + eg: BaseExceptionGroup, exc_type: type[BaseException], *, fix_tracebacks: bool = False +) -> BaseException | None: + """ + Find the first exception of a specific type in an ExceptionGroup. + + Args: + eg: The ExceptionGroup to search + exc_type: The exception type to find + fix_tracebacks: Whether to fix tracebacks in leaf exceptions + + Returns: + The first matching exception, or None if not found + """ + for exc in leaf_exceptions(eg, fix_tracebacks=fix_tracebacks): + if isinstance(exc, exc_type): + return exc + return None + + # https://peps.python.org/pep-0654/ def leaf_exceptions(self: BaseExceptionGroup, *, fix_tracebacks: bool = True) -> list[BaseException]: """ diff --git a/packages/jumpstarter-cli/jumpstarter_cli/j.py b/packages/jumpstarter-cli/jumpstarter_cli/j.py index 1cf7befdc..a27e24fe5 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/j.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/j.py @@ -6,21 +6,32 @@ import click from anyio import create_task_group, get_cancelled_exc_class, run, to_thread from anyio.from_thread import BlockingPortal -from jumpstarter_cli_common.exceptions import async_handle_exceptions, leaf_exceptions +from jumpstarter_cli_common.exceptions import ( + ClickExceptionRed, + async_handle_exceptions, + find_exception_in_group, + leaf_exceptions, +) from jumpstarter_cli_common.signal import signal_handler from rich import traceback +from jumpstarter.common.exceptions import EnvironmentVariableNotSetError from jumpstarter.utils.env import env_async async def j_async(): @async_handle_exceptions async def cli(): - async with BlockingPortal() as portal: - with ExitStack() as stack: - async with env_async(portal, stack) as client: - await to_thread.run_sync(lambda: client.cli()(standalone_mode=False)) - + try: + async with BlockingPortal() as portal: + with ExitStack() as stack: + async with env_async(portal, stack) as client: + await to_thread.run_sync(lambda: client.cli()(standalone_mode=False)) + except BaseExceptionGroup as eg: + # Handle exceptions wrapped in ExceptionGroup (e.g., from task groups) + if exc := find_exception_in_group(eg, EnvironmentVariableNotSetError): + raise ClickExceptionRed(f"Error: the j command must be used inside a jmp shell: {exc}") from eg + raise eg try: async with create_task_group() as tg: tg.start_soon(signal_handler, tg.cancel_scope) @@ -29,7 +40,6 @@ async def cli(): await cli() finally: tg.cancel_scope.cancel() - except* click.ClickException as excgroup: for exc in leaf_exceptions(excgroup): cast(click.ClickException, exc).show() diff --git a/packages/jumpstarter/jumpstarter/common/exceptions.py b/packages/jumpstarter/jumpstarter/common/exceptions.py index 4291e935e..f2076c158 100644 --- a/packages/jumpstarter/jumpstarter/common/exceptions.py +++ b/packages/jumpstarter/jumpstarter/common/exceptions.py @@ -73,3 +73,9 @@ class ReauthenticationFailed(JumpstarterException): """Raised when a re-authentication fails.""" pass + + +class EnvironmentVariableNotSetError(JumpstarterException): + """Raised when a environment variable is not set.""" + + pass diff --git a/packages/jumpstarter/jumpstarter/utils/env.py b/packages/jumpstarter/jumpstarter/utils/env.py index 83d7590bc..c6977b7e9 100644 --- a/packages/jumpstarter/jumpstarter/utils/env.py +++ b/packages/jumpstarter/jumpstarter/utils/env.py @@ -4,6 +4,7 @@ from anyio.from_thread import start_blocking_portal from jumpstarter.client import client_from_path +from jumpstarter.common.exceptions import EnvironmentVariableNotSetError from jumpstarter.config.client import ClientConfigV1Alpha1Drivers from jumpstarter.config.env import JUMPSTARTER_HOST @@ -19,7 +20,7 @@ async def env_async(portal, stack): """ host = os.environ.get(JUMPSTARTER_HOST, None) if host is None: - raise RuntimeError(f"{JUMPSTARTER_HOST} not set") + raise EnvironmentVariableNotSetError(f"{JUMPSTARTER_HOST} not set") drivers = ClientConfigV1Alpha1Drivers()