From d1797149bab0810e1fb0d50148eab130ccdeb886 Mon Sep 17 00:00:00 2001 From: Sevrain Date: Sat, 20 Dec 2025 19:04:06 +0200 Subject: [PATCH 1/3] fix: allow `None` as argument for `kbi_msg` This commit addresses the issue of the keyboard interrupt message always printing, even in cases where it's not needed and is explicitly set to an empty string. This allows users to disable/replace said print when needed, while avoiding the redundant newline that's currenly being printed when `kbi_msg=""`. This commit also introduces the usage of `TYPE_CHECKING` to improve performance in the changed modules. --- questionary/form.py | 21 ++++++++++++++------- questionary/prompt.py | 27 ++++++++++++++------------- questionary/question.py | 18 ++++++++++++++---- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/questionary/form.py b/questionary/form.py index 17d13071..1a4b8cf0 100644 --- a/questionary/form.py +++ b/questionary/form.py @@ -1,7 +1,8 @@ -from typing import Any -from typing import Dict +from typing import TYPE_CHECKING from typing import NamedTuple -from typing import Sequence + +if TYPE_CHECKING: + from typing import Any, Dict, Sequence, Union from questionary.constants import DEFAULT_KBI_MESSAGE from questionary.question import Question @@ -77,7 +78,9 @@ async def unsafe_ask_async(self, patch_stdout: bool = False) -> Dict[str, Any]: } def ask( - self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE + self, + patch_stdout: bool = False, + kbi_msg: Union[str, None] = DEFAULT_KBI_MESSAGE, ) -> Dict[str, Any]: """Ask the questions synchronously and return user response. @@ -93,11 +96,14 @@ def ask( try: return self.unsafe_ask(patch_stdout) except KeyboardInterrupt: - print(kbi_msg) + if kbi_msg is not None: + print(kbi_msg) return {} async def ask_async( - self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE + self, + patch_stdout: bool = False, + kbi_msg: Union[str, None] = DEFAULT_KBI_MESSAGE, ) -> Dict[str, Any]: """Ask the questions using asyncio and return user response. @@ -113,5 +119,6 @@ async def ask_async( try: return await self.unsafe_ask_async(patch_stdout) except KeyboardInterrupt: - print(kbi_msg) + if kbi_msg is not None: + print(kbi_msg) return {} diff --git a/questionary/prompt.py b/questionary/prompt.py index 561ce7f2..8d1f477b 100644 --- a/questionary/prompt.py +++ b/questionary/prompt.py @@ -1,10 +1,8 @@ -from typing import Any +from typing import TYPE_CHECKING from typing import Callable -from typing import Dict -from typing import Iterable -from typing import Mapping -from typing import Optional -from typing import Union + +if TYPE_CHECKING: + from typing import Any, Dict, Iterable, Mapping, Optional, Union from prompt_toolkit.output import ColorDepth @@ -68,7 +66,7 @@ def parse_question_config( return None except Exception as exception: raise ValueError( - f"Problem in 'when' check of " f"{name} question: {exception}" + f"Problem in 'when' check of {name} question: {exception}" ) from exception else: raise ValueError("'when' needs to be function that accepts a dict argument") @@ -124,8 +122,7 @@ def on_answer(answer): answer = _filter(answer) except Exception as exception: raise ValueError( - f"Problem processing 'filter' of {name} " - f"question: {exception}" + f"Problem processing 'filter' of {name} question: {exception}" ) from exception answers[name] = answer @@ -137,7 +134,7 @@ async def prompt_async( answers: Optional[Mapping[str, Any]] = None, patch_stdout: bool = False, true_color: bool = False, - kbi_msg: str = DEFAULT_KBI_MESSAGE, + kbi_msg: Union[str, None] = DEFAULT_KBI_MESSAGE, **kwargs: Any, ) -> Dict[str, Any]: """Prompt the user for input on all the questions using asyncio. @@ -168,6 +165,7 @@ async def prompt_async( are printing to stdout. kbi_msg: The message to be printed on a keyboard interrupt. + true_color: Use true color output. color_depth: Color depth to use. If ``true_color`` is set to true then this @@ -189,7 +187,8 @@ async def prompt_async( questions, answers, patch_stdout, true_color, **kwargs ) except KeyboardInterrupt: - print(kbi_msg) + if kbi_msg is not None: + print(kbi_msg) return {} @@ -198,7 +197,7 @@ def prompt( answers: Optional[Mapping[str, Any]] = None, patch_stdout: bool = False, true_color: bool = False, - kbi_msg: str = DEFAULT_KBI_MESSAGE, + kbi_msg: Union[str, None] = DEFAULT_KBI_MESSAGE, **kwargs: Any, ) -> Dict[str, Any]: """Prompt the user for input on all the questions. @@ -229,6 +228,7 @@ def prompt( are printing to stdout. kbi_msg: The message to be printed on a keyboard interrupt. + true_color: Use true color output. color_depth: Color depth to use. If ``true_color`` is set to true then this @@ -248,7 +248,8 @@ def prompt( try: return unsafe_prompt(questions, answers, patch_stdout, true_color, **kwargs) except KeyboardInterrupt: - print(kbi_msg) + if kbi_msg is not None: + print(kbi_msg) return {} diff --git a/questionary/question.py b/questionary/question.py index 978ec8c2..901bc1b1 100644 --- a/questionary/question.py +++ b/questionary/question.py @@ -1,6 +1,10 @@ import sys +from typing import TYPE_CHECKING from typing import Any +if TYPE_CHECKING: + from typing import Union + import prompt_toolkit.patch_stdout from prompt_toolkit import Application @@ -24,7 +28,9 @@ def __init__(self, application: "Application[Any]") -> None: self.default = None async def ask_async( - self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE + self, + patch_stdout: bool = False, + kbi_msg: Union[str, None] = DEFAULT_KBI_MESSAGE, ) -> Any: """Ask the question using asyncio and return user response. @@ -42,11 +48,14 @@ async def ask_async( sys.stdout.flush() return await self.unsafe_ask_async(patch_stdout) except KeyboardInterrupt: - print("{}".format(kbi_msg)) + if kbi_msg is not None: + print(f"{kbi_msg}") return None def ask( - self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE + self, + patch_stdout: bool = False, + kbi_msg: Union[str, None] = DEFAULT_KBI_MESSAGE, ) -> Any: """Ask the question synchronously and return user response. @@ -63,7 +72,8 @@ def ask( try: return self.unsafe_ask(patch_stdout) except KeyboardInterrupt: - print("{}".format(kbi_msg)) + if kbi_msg is not None: + print(f"{kbi_msg}") return None def unsafe_ask(self, patch_stdout: bool = False) -> Any: From a73f9510a0673f1309fd1cf4a85509edd52886d1 Mon Sep 17 00:00:00 2001 From: Sevrain Date: Sat, 27 Dec 2025 23:06:58 +0200 Subject: [PATCH 2/3] test: add kbi_msg=None tests and restore previous typing imports This commit adds tests to the `Form` `Prompt` and `Question` classes to ensure no message is printed when a KeyboardInterrupt is raised during an `ask()`/`ask_async()` call where `kbi_msg=None`. --- questionary/form.py | 8 +++--- questionary/prompt.py | 10 +++++--- questionary/question.py | 5 +--- tests/test_form.py | 37 ++++++++++++++++++++++++++++ tests/test_prompt.py | 54 +++++++++++++++++++++++++++++++++++++++++ tests/test_question.py | 35 ++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 12 deletions(-) diff --git a/questionary/form.py b/questionary/form.py index 1a4b8cf0..a79e5df0 100644 --- a/questionary/form.py +++ b/questionary/form.py @@ -1,8 +1,8 @@ -from typing import TYPE_CHECKING +from typing import Any +from typing import Dict from typing import NamedTuple - -if TYPE_CHECKING: - from typing import Any, Dict, Sequence, Union +from typing import Sequence +from typing import Union from questionary.constants import DEFAULT_KBI_MESSAGE from questionary.question import Question diff --git a/questionary/prompt.py b/questionary/prompt.py index 8d1f477b..ac49e5fb 100644 --- a/questionary/prompt.py +++ b/questionary/prompt.py @@ -1,8 +1,10 @@ -from typing import TYPE_CHECKING +from typing import Any from typing import Callable - -if TYPE_CHECKING: - from typing import Any, Dict, Iterable, Mapping, Optional, Union +from typing import Dict +from typing import Iterable +from typing import Mapping +from typing import Optional +from typing import Union from prompt_toolkit.output import ColorDepth diff --git a/questionary/question.py b/questionary/question.py index 901bc1b1..c2a757d9 100644 --- a/questionary/question.py +++ b/questionary/question.py @@ -1,9 +1,6 @@ import sys -from typing import TYPE_CHECKING from typing import Any - -if TYPE_CHECKING: - from typing import Union +from typing import Union import prompt_toolkit.patch_stdout from prompt_toolkit import Application diff --git a/tests/test_form.py b/tests/test_form.py index ece23219..58b075c8 100644 --- a/tests/test_form.py +++ b/tests/test_form.py @@ -1,3 +1,6 @@ +import asyncio +from unittest.mock import patch + from prompt_toolkit.output import DummyOutput from pytest import fail @@ -77,3 +80,37 @@ def run(inp): fail("Keyboard Interrupt should be caught by `ask()`") execute_with_input_pipe(run) + + +def test_no_keyboard_interrupt_message_in_ask() -> None: + """ + Test no message printed when `kbi_msg` is None in `ask()`. + """ + def run(inp): + inp.send_text(KeyInputs.CONTROLC) + f = example_form(inp) + + with patch("builtins.print") as mock_print: + result = f.ask(kbi_msg=None) + + mock_print.assert_not_called() + assert result == {} + + execute_with_input_pipe(run) + + +def test_no_keyboard_interrupt_message_in_ask_async() -> None: + """ + Test no message printed when `kbi_msg` is None in `ask_async()`. + """ + def run(inp): + inp.send_text(KeyInputs.CONTROLC) + f = example_form(inp) + + with patch("builtins.print") as mock_print: + result = asyncio.run(f.ask_async(kbi_msg=None)) + + mock_print.assert_not_called() + assert result == {} + + execute_with_input_pipe(run) diff --git a/tests/test_prompt.py b/tests/test_prompt.py index 9a9a84be..03fe22ba 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -1,7 +1,11 @@ +import asyncio +from unittest.mock import patch + import pytest from questionary.prompt import PromptParameterException from questionary.prompt import prompt +from questionary.prompt import prompt_async from tests.utils import patched_prompt @@ -76,3 +80,53 @@ def test_print_with_name(): questions = [{"name": "hello", "type": "print", "message": "Hello World"}] result = patched_prompt(questions, "") assert result == {"hello": None} + + +@patch("builtins.print") +@patch("questionary.prompt.unsafe_prompt", side_effect=KeyboardInterrupt) +def test_no_keyboard_interrupt_message_in_prompt( + mock_unsafe_prompt, mock_print +) -> None: + """ + Test no message printed when `kbi_msg` is None in `prompt()`. + + Args: + mock_unsafe_prompt: A mock of the internal `unsafe_prompt()` call. + raises a KeyboardInterrupt. + + mock_print: A mock of Python's builtin `print()` function. + """ + # Act + result = prompt(questions={}, kbi_msg=None) + + # Verify internal functions were properly called + mock_unsafe_prompt.assert_called_once() # Raises KeyboardInterrupt + mock_print.assert_not_called() + + # Verify result + assert result == {} + + +@patch("builtins.print") +@patch("questionary.prompt.unsafe_prompt_async", side_effect=KeyboardInterrupt) +def test_no_keyboard_interrupt_message_in_prompt_async( + mock_unsafe_prompt_async, mock_print +) -> None: + """ + Test no message printed when `kbi_msg` is None in `prompt_async()`. + + Args: + mock_unsafe_prompt_async: A mock of the internal `unsafe_prompt_async()` call. + raises a KeyboardInterrupt. + + mock_print: A mock of Python's builtin `print()` function. + """ + # Act + result = asyncio.run(prompt_async(questions={}, kbi_msg=None)) + + # Verify internal functions were properly called + mock_unsafe_prompt_async.assert_called_once() # Raises KeyboardInterrupt + mock_print.assert_not_called() + + # Verify result + assert result == {} diff --git a/tests/test_question.py b/tests/test_question.py index f0f9c223..5fb78da0 100644 --- a/tests/test_question.py +++ b/tests/test_question.py @@ -1,5 +1,6 @@ import asyncio import platform +from unittest.mock import patch import pytest from prompt_toolkit.output import DummyOutput @@ -94,3 +95,37 @@ def run(inp): assert response == "Hello\nworld" execute_with_input_pipe(run) + + +def test_no_keyboard_interrupt_message_in_ask(): + """ + Test no message printed when `kbi_msg` is None in `ask()`. + """ + def run(inp): + inp.send_text(KeyInputs.CONTROLC) + question = text("Hello?", input=inp, output=DummyOutput()) + + with patch("builtins.print") as mock_print: + result = question.ask(kbi_msg=None) + + mock_print.assert_not_called() + assert result is None + + execute_with_input_pipe(run) + + +def test_no_keyboard_interrupt_message_in_ask_async(): + """ + Test no message printed when `kbi_msg` is None in `ask_async()`. + """ + def run(inp): + inp.send_text(KeyInputs.CONTROLC) + question = text("Hello?", input=inp, output=DummyOutput()) + + with patch("builtins.print") as mock_print: + result = asyncio.run(question.ask_async(kbi_msg=None)) + + mock_print.assert_not_called() + assert result is None + + execute_with_input_pipe(run) From b72b39f4b35206cc267bf8ff4477fb3d4cd3ee51 Mon Sep 17 00:00:00 2001 From: Kian Cross Date: Wed, 28 Jan 2026 10:34:48 +0000 Subject: [PATCH 3/3] Fix formatting --- tests/test_form.py | 2 ++ tests/test_question.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/test_form.py b/tests/test_form.py index 58b075c8..15859a69 100644 --- a/tests/test_form.py +++ b/tests/test_form.py @@ -86,6 +86,7 @@ def test_no_keyboard_interrupt_message_in_ask() -> None: """ Test no message printed when `kbi_msg` is None in `ask()`. """ + def run(inp): inp.send_text(KeyInputs.CONTROLC) f = example_form(inp) @@ -103,6 +104,7 @@ def test_no_keyboard_interrupt_message_in_ask_async() -> None: """ Test no message printed when `kbi_msg` is None in `ask_async()`. """ + def run(inp): inp.send_text(KeyInputs.CONTROLC) f = example_form(inp) diff --git a/tests/test_question.py b/tests/test_question.py index 5fb78da0..74971765 100644 --- a/tests/test_question.py +++ b/tests/test_question.py @@ -101,6 +101,7 @@ def test_no_keyboard_interrupt_message_in_ask(): """ Test no message printed when `kbi_msg` is None in `ask()`. """ + def run(inp): inp.send_text(KeyInputs.CONTROLC) question = text("Hello?", input=inp, output=DummyOutput()) @@ -118,6 +119,7 @@ def test_no_keyboard_interrupt_message_in_ask_async(): """ Test no message printed when `kbi_msg` is None in `ask_async()`. """ + def run(inp): inp.send_text(KeyInputs.CONTROLC) question = text("Hello?", input=inp, output=DummyOutput())