From 829c9614fdd92e42ff132b82035b1fa6bdd0be2a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 16:00:35 +0000 Subject: [PATCH] Handle KeyboardInterrupt during abort echo A second Ctrl-C while Command.main prints Aborted! after prompt() is cancelled could escape as an unhandled KeyboardInterrupt. Fixes #3802. Co-authored-by: cnbei --- CHANGES.md | 4 ++++ src/click/core.py | 10 ++++++++-- tests/test_utils/test_prompt.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c466d22cf..9965a770d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ Unreleased +- Handle a second `KeyboardInterrupt` while printing `Aborted!` after + `prompt()` is cancelled with Ctrl-C. Previously that interrupt could + escape `Command.main` as an unhandled exception. {issue}`3802` + ## Version 8.5.0 Released 2026-08-24 diff --git a/src/click/core.py b/src/click/core.py index de129ec2c..50338b60e 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1561,7 +1561,10 @@ def main( # by its truthiness/falsiness ctx.exit() except (EOFError, KeyboardInterrupt) as e: - echo(file=sys.stderr) + try: + echo(file=sys.stderr) + except KeyboardInterrupt: + pass raise Abort() from e except ClickException as e: if not standalone_mode: @@ -1591,7 +1594,10 @@ def main( except Abort: if not standalone_mode: raise - echo(_("Aborted!"), file=sys.stderr) + try: + echo(_("Aborted!"), file=sys.stderr) + except KeyboardInterrupt: + pass sys.exit(1) def _main_shell_completion( diff --git a/tests/test_utils/test_prompt.py b/tests/test_utils/test_prompt.py index 1b223afc3..bea090d53 100644 --- a/tests/test_utils/test_prompt.py +++ b/tests/test_utils/test_prompt.py @@ -87,6 +87,38 @@ def capture(text): assert received == [expected_prompt] +def test_prompt_abort_echo_survives_keyboard_interrupt(monkeypatch): + """Ctrl-C at a prompt becomes Abort. A second KeyboardInterrupt + during the Aborted! echo (echo -> isatty) must still exit 1. + + https://github.com/pallets/click/issues/3802 + """ + + @click.command() + def cli(): + click.prompt("name") + + def interrupt(_text): + raise KeyboardInterrupt() + + monkeypatch.setattr("click.termui.visible_prompt_func", interrupt) + + stderr = StringIO() + + def interrupt_isatty(): + raise KeyboardInterrupt() + + stderr.isatty = interrupt_isatty # type: ignore[method-assign] + monkeypatch.setattr(sys, "stderr", stderr) + + try: + cli.main([]) + except SystemExit as e: + assert e.code == 1 + except KeyboardInterrupt: + pytest.fail("KeyboardInterrupt escaped Command.main during abort") + + def test_prompts_eof(runner): """If too few lines of input are given, prompt should exit, not hang."""