From 98b54f8cca1f25c039f1a5a5415be4b9820c4f7a Mon Sep 17 00:00:00 2001 From: svlandeg Date: Fri, 28 Aug 2026 11:43:02 +0200 Subject: [PATCH 1/2] introduce exceptions module in Typer --- typer/__init__.py | 5 +++-- typer/_click/core.py | 10 ++-------- typer/_click/exceptions.py | 24 ++---------------------- typer/_click/termui.py | 3 ++- typer/_completion_shared.py | 9 +++++---- typer/core.py | 18 ++++++------------ typer/exceptions.py | 26 ++++++++++++++++++++++++++ 7 files changed, 46 insertions(+), 49 deletions(-) create mode 100644 typer/exceptions.py diff --git a/typer/__init__.py b/typer/__init__.py index 462f2eb00f..00fe417251 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -5,9 +5,7 @@ from shutil import get_terminal_size as get_terminal_size from . import colors as colors -from ._click.exceptions import Abort as Abort from ._click.exceptions import BadParameter as BadParameter -from ._click.exceptions import Exit as Exit from ._click.termui import confirm as confirm from ._click.termui import getchar as getchar from ._click.termui import progressbar as progressbar @@ -19,6 +17,9 @@ from ._click.utils import get_app_dir as get_app_dir from ._click.utils import get_binary_stream as get_binary_stream from ._click.utils import get_text_stream as get_text_stream +from .exceptions import Abort as Abort +from .exceptions import Exit as Exit +from .exceptions import TyperException as TyperException from .main import Typer as Typer from .main import launch as launch from .main import run as run diff --git a/typer/_click/core.py b/typer/_click/core.py index ada074bcc3..54dc95fbd7 100644 --- a/typer/_click/core.py +++ b/typer/_click/core.py @@ -16,15 +16,9 @@ overload, ) +from ..exceptions import Abort, Exit from . import types -from .exceptions import ( - Abort, - BadParameter, - Exit, - MissingParameter, - NoArgsIsHelpError, - UsageError, -) +from .exceptions import BadParameter, MissingParameter, NoArgsIsHelpError, UsageError from .formatting import HelpFormatter from .globals import pop_context, push_context from .parser import _OptionParser diff --git a/typer/_click/exceptions.py b/typer/_click/exceptions.py index 75ba2296bb..b05e3038f4 100644 --- a/typer/_click/exceptions.py +++ b/typer/_click/exceptions.py @@ -1,6 +1,7 @@ from collections.abc import Sequence from typing import IO, TYPE_CHECKING, Any, Union +from ..exceptions import TyperException from ._compat import get_text_stderr from .globals import resolve_color_default from .utils import echo, format_filename @@ -16,20 +17,14 @@ def _join_param_hints(param_hint: Sequence[str] | str | None) -> str | None: return param_hint -class ClickException(Exception): +class ClickException(TyperException): """An exception that Click can handle and show to the user.""" - exit_code = 1 - def __init__(self, message: str) -> None: super().__init__(message) # The context will be removed by the time we print the message, so cache # the color settings here to be used later on (in `show`) self.show_color: bool | None = resolve_color_default() - self.message = message - - def format_message(self) -> str: - return self.message def __str__(self) -> str: return self.message @@ -243,18 +238,3 @@ def __init__(self, filename: str, hint: str | None = None) -> None: def format_message(self) -> str: return f"Could not open file {self.ui_filename!r}: {self.message}" - - -class Abort(RuntimeError): - """An internal signalling exception that signals Click to abort.""" - - -class Exit(RuntimeError): - """An exception that indicates that the application should exit with some - status code. - """ - - __slots__ = ("exit_code",) - - def __init__(self, code: int = 0) -> None: - self.exit_code: int = code diff --git a/typer/_click/termui.py b/typer/_click/termui.py index 0a8c82574d..8426ea6691 100644 --- a/typer/_click/termui.py +++ b/typer/_click/termui.py @@ -3,7 +3,8 @@ from contextlib import AbstractContextManager from typing import IO, TYPE_CHECKING, Any, AnyStr, TextIO, TypeVar, overload -from .exceptions import Abort, UsageError +from ..exceptions import Abort +from .exceptions import UsageError from .globals import resolve_color_default from .types import ParamType, convert_type from .utils import LazyFile, echo diff --git a/typer/_completion_shared.py b/typer/_completion_shared.py index 8d2c19715c..4b6d5e47b1 100644 --- a/typer/_completion_shared.py +++ b/typer/_completion_shared.py @@ -8,6 +8,7 @@ from . import _click from ._click.globals import get_current_context +from .exceptions import Exit class Shells(str, Enum): @@ -81,7 +82,7 @@ def get_completion_script(*, prog_name: str, complete_var: str, shell: str) -> s script = _completion_scripts.get(shell) if script is None: _click.echo(f"Shell {shell} not supported.", err=True) - raise _click.exceptions.Exit(1) + raise Exit(1) return ( script % { @@ -175,7 +176,7 @@ def install_powershell(*, prog_name: str, complete_var: str, shell: str) -> Path ) if result.returncode != 0: # pragma: no cover _click.echo("Couldn't get PowerShell user profile", err=True) - raise _click.exceptions.Exit(result.returncode) + raise Exit(result.returncode) path_str = "" if isinstance(result.stdout, str): # pragma: no cover path_str = result.stdout @@ -188,7 +189,7 @@ def install_powershell(*, prog_name: str, complete_var: str, shell: str) -> Path pass if not path_str: # pragma: no cover _click.echo("Couldn't decode the path automatically", err=True) - raise _click.exceptions.Exit(1) + raise Exit(1) path_obj = Path(path_str.strip()) parent_dir: Path = path_obj.parent parent_dir.mkdir(parents=True, exist_ok=True) @@ -234,7 +235,7 @@ def install( return shell, installed_path else: _click.echo(f"Shell {shell} is not supported.") - raise _click.exceptions.Exit(1) + raise Exit(1) def _get_shell_name() -> str | None: diff --git a/typer/core.py b/typer/core.py index 1bfc399505..97f96d89ab 100644 --- a/typer/core.py +++ b/typer/core.py @@ -18,6 +18,7 @@ from ._click.parser import _OptionParser from ._click.shell_completion import CompletionItem from ._typing import Literal +from .exceptions import Abort, Exit from .utils import parse_boolean_env_var MarkupMode = Literal["markdown", "rich", None] @@ -165,13 +166,10 @@ def _main( rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE, **extra: Any, ) -> Any: - # Typer override, duplicated from _click.main() to handle custom rich exceptions - # Verify that the environment is configured correctly, or reject - # further execution to avoid a broken script. + # Originally copied from _click.main(), adopted to handle custom rich exceptions etc if args is None: args = sys.argv[1:] - # Covered in Click tests if os.name == "nt" and windows_expand_args: # pragma: no cover args = _click.utils._expand_args(args) else: @@ -199,20 +197,18 @@ def _main( ctx.exit() except EOFError as e: _click.echo(file=sys.stderr) - raise _click.exceptions.Abort() from e + raise Abort() from e except KeyboardInterrupt as e: - raise _click.exceptions.Exit(130) from e + raise Exit(130) from e except _click.exceptions.ClickException as e: if not standalone_mode: raise - # Typer override if HAS_RICH and rich_markup_mode is not None: from . import rich_utils rich_utils.rich_format_error(e) else: e.show() - # Typer override end sys.exit(e.exit_code) except OSError as e: if e.errno == errno.EPIPE: @@ -221,7 +217,7 @@ def _main( sys.exit(1) else: raise - except _click.exceptions.Exit as e: + except Exit as e: if standalone_mode: sys.exit(e.exit_code) else: @@ -234,17 +230,15 @@ def _main( # `ctx.exit(1)` and to `return 1`, the caller won't be able to # tell the difference between the two return e.exit_code - except _click.exceptions.Abort: + except Abort: if not standalone_mode: raise - # Typer override if HAS_RICH and rich_markup_mode is not None: from . import rich_utils rich_utils.rich_abort_error() else: _click.echo(_("Aborted!"), file=sys.stderr) - # Typer override end sys.exit(1) diff --git a/typer/exceptions.py b/typer/exceptions.py new file mode 100644 index 0000000000..be6d00a03e --- /dev/null +++ b/typer/exceptions.py @@ -0,0 +1,26 @@ +class TyperException(Exception): + """A Typer-specific exception""" + + exit_code = 1 + + def __init__(self, message: str) -> None: + super().__init__(message) + self.message = message + + def format_message(self) -> str: + return self.message + + +class Abort(RuntimeError): + """An internal signalling exception that signals Typer to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + """ + + __slots__ = ("exit_code",) + + def __init__(self, code: int = 0) -> None: + self.exit_code: int = code From f99492ee39d2445970952ce37b7fc3633c36363a Mon Sep 17 00:00:00 2001 From: svlandeg Date: Fri, 28 Aug 2026 11:52:05 +0200 Subject: [PATCH 2/2] also move __str__ --- typer/_click/exceptions.py | 3 --- typer/exceptions.py | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/typer/_click/exceptions.py b/typer/_click/exceptions.py index b05e3038f4..5999226ae2 100644 --- a/typer/_click/exceptions.py +++ b/typer/_click/exceptions.py @@ -26,9 +26,6 @@ def __init__(self, message: str) -> None: # the color settings here to be used later on (in `show`) self.show_color: bool | None = resolve_color_default() - def __str__(self) -> str: - return self.message - def show(self, file: IO[Any] | None = None) -> None: if file is None: file = get_text_stderr() diff --git a/typer/exceptions.py b/typer/exceptions.py index be6d00a03e..47665b60a7 100644 --- a/typer/exceptions.py +++ b/typer/exceptions.py @@ -7,6 +7,9 @@ def __init__(self, message: str) -> None: super().__init__(message) self.message = message + def __str__(self) -> str: + return self.message + def format_message(self) -> str: return self.message