diff --git a/src/prompt_toolkit/shortcuts/prompt.py b/src/prompt_toolkit/shortcuts/prompt.py index f14ecd80c..2e3ca8397 100644 --- a/src/prompt_toolkit/shortcuts/prompt.py +++ b/src/prompt_toolkit/shortcuts/prompt.py @@ -225,6 +225,13 @@ class CompleteStyle(str, Enum): _T = TypeVar("_T") +class _Unset: + pass + + +_UNSET = _Unset() + + class PromptSession(Generic[_T]): """ PromptSession for a prompt application, which can be used as a GNU Readline @@ -910,7 +917,7 @@ def prompt( validate_while_typing: FilterOrBool | None = None, complete_style: CompleteStyle | None = None, auto_suggest: AutoSuggest | None = None, - validator: Validator | None = None, + validator: Validator | None | _Unset = _UNSET, clipboard: Clipboard | None = None, mouse_support: FilterOrBool | None = None, input_processors: list[Processor] | None = None, @@ -940,10 +947,10 @@ class itself. For these, passing in ``None`` will keep the current attribute for the session, which means that it applies to the current, but also to the next prompts. - Note that in order to erase a ``Completer``, ``Validator`` or - ``AutoSuggest``, you can't use ``None``. Instead pass in a - ``DummyCompleter``, ``DummyValidator`` or ``DummyAutoSuggest`` instance - respectively. For a ``Lexer`` you can pass in an empty ``SimpleLexer``. + Note that in order to erase a ``Completer`` or ``AutoSuggest``, you + can't use ``None``. Instead pass in a ``DummyCompleter`` or + ``DummyAutoSuggest`` instance respectively. For a ``Lexer`` you can + pass in an empty ``SimpleLexer``. Additional arguments, specific for this prompt: @@ -1024,8 +1031,8 @@ class itself. For these, passing in ``None`` will keep the current self.complete_style = complete_style if auto_suggest is not None: self.auto_suggest = auto_suggest - if validator is not None: - self.validator = validator + if validator is not _UNSET: + self.validator = cast(Validator | None, validator) if clipboard is not None: self.clipboard = clipboard if mouse_support is not None: @@ -1150,7 +1157,7 @@ async def prompt_async( validate_while_typing: FilterOrBool | None = None, complete_style: CompleteStyle | None = None, auto_suggest: AutoSuggest | None = None, - validator: Validator | None = None, + validator: Validator | None | _Unset = _UNSET, clipboard: Clipboard | None = None, mouse_support: FilterOrBool | None = None, input_processors: list[Processor] | None = None, @@ -1221,8 +1228,8 @@ async def prompt_async( self.complete_style = complete_style if auto_suggest is not None: self.auto_suggest = auto_suggest - if validator is not None: - self.validator = validator + if validator is not _UNSET: + self.validator = cast(Validator | None, validator) if clipboard is not None: self.clipboard = clipboard if mouse_support is not None: diff --git a/tests/test_cli.py b/tests/test_cli.py index a876f2993..6cb88d317 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,6 +5,7 @@ from __future__ import annotations +import asyncio from functools import partial import pytest @@ -19,6 +20,7 @@ from prompt_toolkit.key_binding.key_bindings import KeyBindings from prompt_toolkit.output import DummyOutput from prompt_toolkit.shortcuts import PromptSession +from prompt_toolkit.validation import Validator def _history(): @@ -71,6 +73,58 @@ def test_simple_text_input(): assert cli.current_buffer.text == "hello" +def test_prompt_validator_none_clears_session_validator(): + validator = Validator.from_callable(lambda text: True) + + with create_pipe_input() as inp: + inp.send_text("hello\r") + session = PromptSession( + input=inp, + output=DummyOutput(), + validator=validator, + ) + + session.prompt(validator=None) + + assert session.validator is None + + +def test_prompt_without_validator_keeps_session_validator(): + validator = Validator.from_callable(lambda text: True) + + with create_pipe_input() as inp: + inp.send_text("hello\r") + session = PromptSession( + input=inp, + output=DummyOutput(), + validator=validator, + ) + + session.prompt() + + assert session.validator is validator + + +def test_prompt_async_validator_none_clears_session_validator(): + validator = Validator.from_callable(lambda text: True) + + async def run_prompt() -> PromptSession[str]: + with create_pipe_input() as inp: + inp.send_text("hello\r") + session = PromptSession( + input=inp, + output=DummyOutput(), + validator=validator, + ) + + await session.prompt_async(validator=None) + + return session + + session = asyncio.run(run_prompt()) + assert session.validator is None + + def test_emacs_cursor_movements(): """ Test cursor movements with Emacs key bindings.