diff --git a/questionary/prompts/confirm.py b/questionary/prompts/confirm.py index cbb5347e..f2570305 100644 --- a/questionary/prompts/confirm.py +++ b/questionary/prompts/confirm.py @@ -18,36 +18,25 @@ def confirm( message: str, - default: bool = True, + default: Optional[bool] = True, qmark: str = DEFAULT_QUESTION_PREFIX, style: Optional[Style] = None, auto_enter: bool = True, instruction: Optional[str] = None, + mandatory: bool = True, **kwargs: Any, ) -> Question: """A yes or no question. The user can either confirm or deny. This question type can be used to prompt the user for a confirmation of a yes-or-no question. If the user just hits enter, the default - value will be returned. - - Example: - >>> import questionary - >>> questionary.confirm("Are you amazed?").ask() - ? Are you amazed? Yes - True - - .. image:: ../images/confirm.gif - - This is just a really basic example, the prompt can be customised using the - parameters. - + value will be returned unless mandatory=True. Args: message: Question text. - default: Default value will be returned if the user just hits - enter. + default: Default value will be returned if the user just hits enter, + unless mandatory=True. qmark: Question prefix displayed in front of the question. By default this is a ``?``. @@ -61,6 +50,10 @@ def confirm( instruction: A message describing how to proceed through the confirmation prompt. + + mandatory: If set to `True`, the user must type either 'y' or 'n'; + pressing Enter without input is not allowed. + Returns: :class:`Question`: Question instance, ready to be prompted (using `.ask()`). """ @@ -72,13 +65,16 @@ def get_prompt_tokens(): tokens = [] tokens.append(("class:qmark", qmark)) - tokens.append(("class:question", " {} ".format(message))) + tokens.append(("class:question", f" {message} ")) if instruction is not None: tokens.append(("class:instruction", instruction)) elif not status["complete"]: - _instruction = YES_OR_NO if default else NO_OR_YES - tokens.append(("class:instruction", "{} ".format(_instruction))) + if mandatory: + tokens.append(("class:instruction", "(Type 'y' or 'n') ")) + else: + _instruction = YES_OR_NO if default else NO_OR_YES + tokens.append(("class:instruction", f"{_instruction} ")) if status["answer"] is not None: answer = YES if status["answer"] else NO @@ -117,10 +113,13 @@ def key_backspace(event): @bindings.add(Keys.ControlM, eager=True) def set_answer(event): - if status["answer"] is None: - status["answer"] = default - - exit_with_result(event) + if mandatory and status["answer"] is None: + # Prevent submission if answer is None and mandatory=True + event.app.invalidate() + else: + if status["answer"] is None: + status["answer"] = default + exit_with_result(event) @bindings.add(Keys.Any) def other(event): diff --git a/tests/prompts/test_confirm.py b/tests/prompts/test_confirm.py index 7345c431..1ab2e21e 100644 --- a/tests/prompts/test_confirm.py +++ b/tests/prompts/test_confirm.py @@ -5,23 +5,8 @@ from tests.utils import feed_cli_with_input -def test_confirm_enter_default_yes(): - message = "Foo message" - text = KeyInputs.ENTER + "\r" - - result, cli = feed_cli_with_input("confirm", message, text) - assert result is True - - -def test_confirm_enter_default_no(): - message = "Foo message" - text = KeyInputs.ENTER + "\r" - - result, cli = feed_cli_with_input("confirm", message, text, default=False) - assert result is False - - def test_confirm_yes(): + """Test standard confirmation with 'y'.""" message = "Foo message" text = "y" + "\r" @@ -30,6 +15,7 @@ def test_confirm_yes(): def test_confirm_no(): + """Test standard confirmation with 'n'.""" message = "Foo message" text = "n" + "\r" @@ -38,6 +24,7 @@ def test_confirm_no(): def test_confirm_big_yes(): + """Test standard confirmation with 'Y'.""" message = "Foo message" text = "Y" + "\r" @@ -46,6 +33,7 @@ def test_confirm_big_yes(): def test_confirm_big_no(): + """Test standard confirmation with 'N'.""" message = "Foo message" text = "N" + "\r" @@ -53,15 +41,8 @@ def test_confirm_big_no(): assert result is False -def test_confirm_random_input(): - message = "Foo message" - text = "my stuff" + KeyInputs.ENTER + "\r" - - result, cli = feed_cli_with_input("confirm", message, text) - assert result is True - - def test_confirm_ctr_c(): + """Test handling of Ctrl+C interruption.""" message = "Foo message" text = KeyInputs.CONTROLC @@ -70,6 +51,7 @@ def test_confirm_ctr_c(): def test_confirm_not_autoenter_yes(): + """Test confirm prompt without auto-enter when user types 'y'.""" message = "Foo message" text = "n" + "y" + KeyInputs.ENTER + "\r" @@ -78,22 +60,25 @@ def test_confirm_not_autoenter_yes(): def test_confirm_not_autoenter_no(): + """Test confirm prompt without auto-enter when user types 'n'.""" message = "Foo message" - text = "n" + "y" + KeyInputs.ENTER + "\r" + text = "n" + KeyInputs.ENTER + "\r" result, cli = feed_cli_with_input("confirm", message, text, auto_enter=False) - assert result is True + assert result is False def test_confirm_not_autoenter_backspace(): + """Test confirm prompt where user backspaces to empty input and retypes.""" message = "Foo message" - text = "n" + KeyInputs.BACK + KeyInputs.ENTER + "\r" + text = "n" + KeyInputs.BACK + "y" + KeyInputs.ENTER + "\r" result, cli = feed_cli_with_input("confirm", message, text, auto_enter=False) assert result is True def test_confirm_instruction(): + """Test confirm prompt with a custom instruction.""" message = "Foo message" text = "Y" + "\r" @@ -101,3 +86,39 @@ def test_confirm_instruction(): "confirm", message, text, instruction="Foo instruction" ) assert result is True + + +def test_confirm_mandatory_yes(): + """Test mandatory confirm prompt when user explicitly types 'yes'.""" + message = "Foo message" + text = "y" + "\r" + + result, cli = feed_cli_with_input("confirm", message, text, mandatory=True) + assert result is True + + +def test_confirm_mandatory_no(): + """Test mandatory confirm prompt when user explicitly types 'no'.""" + message = "Foo message" + text = "n" + "\r" + + result, cli = feed_cli_with_input("confirm", message, text, mandatory=True) + assert result is False + + +def test_confirm_mandatory_reject_empty(): + """Test mandatory confirm prompt rejects empty input.""" + message = "Foo message" + text = KeyInputs.ENTER + "y" + "\r" + + result, cli = feed_cli_with_input("confirm", message, text, mandatory=True) + assert result is True + + +def test_confirm_mandatory_reject_invalid_input(): + """Test mandatory confirm prompt rejects invalid input.""" + message = "Foo message" + text = "x" + KeyInputs.ENTER + "y" + "\r" + + result, cli = feed_cli_with_input("confirm", message, text, mandatory=True) + assert result is True