prompt-toolkit / python-prompt-toolkit

Library for building powerful interactive command line applications in Python
https://python-prompt-toolkit.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
9.11k stars 718 forks source link

input_dialog validation #1715

Open sandrospadaro opened 1 year ago

sandrospadaro commented 1 year ago

Validation does not work with input_dialog.

how to reproduce the issue

from prompt_toolkit.validation import Validator
from prompt_toolkit.shortcuts import input_dialog
from prompt_toolkit.validation import Validator

def is_number(text):
    return text.isdigit()

validator = Validator.from_callable(
    is_number,
    error_message='This input contains non-numeric characters')

age = input_dialog(title="Age", text="Your age:",
                   validator=validator).run()

print(f"Your age: {age}")

Fill the input dialog with some string (eg: abc) and press tab key to move focus on OK button. Then press enter key.

Output

Your age: abc

Output expected

Error message on focus lost

About the environment

$ python -V
Python 3.11.1
$ pip freeze
prompt-toolkit==3.0.36
wcwidth==0.2.6
x24git commented 1 year ago

Hi there, I created a PR that fixes the validation issue for input dialogs #1747. Until the PR is merged you can make your own input_dialog class that fixes the issue while using the base prompt-toolkit package


class CustomValidationToolbar:
    def __init__(self, show_position: bool = False, buffer: Buffer = None) -> None:
        def get_formatted_text() -> StyleAndTextTuples:
            # If buffer not specified, use the currently focused buffer
            if buffer is None:
                buff = get_app().current_buffer
            else:
                buff = buffer
            if buff.validation_error:
                if show_position:
                    row, column = buff.document.translate_index_to_position(
                        buff.validation_error.cursor_position
                    )

                    text = "{} (line={} column={})".format(
                        buff.validation_error.message,
                        row + 1,
                        column + 1,
                    )
                else:
                    text = buff.validation_error.message

                return [("class:validation-toolbar", text)]
            else:
                return []

        self.control = FormattedTextControl(get_formatted_text)

        self.container = ConditionalContainer(
            content=Window(self.control, height=1), filter=True
        )

    def __pt_container__(self) -> Container:
        return self.container

def input_dialog(
    title: AnyFormattedText = "",
    text: AnyFormattedText = "",
    ok_text: str = "OK",
    cancel_text: str = "Cancel",
    completer: Completer | None = None,
    validator: Validator | None = None,
    validate_while_typing: FilterOrBool = False,
    password: FilterOrBool = False,
    style: BaseStyle | None = None,
    default: str = "",
) -> Application[str]:

    def accept(buf: Buffer) -> bool:
        get_app().layout.focus(ok_button)
        return True  # Keep text.

    def ok_handler() -> None:
        textfield.buffer.validate()  # validate one final time before exiting
        if textfield.buffer.validation_error is None:
            get_app().exit(result=textfield.text)

    ok_button = Button(text=ok_text, handler=ok_handler)
    cancel_button = Button(text=cancel_text, handler=_return_none)

    textfield = TextArea(
        text=default,
        multiline=False,
        password=password,
        completer=completer,
        validator=validator,
        accept_handler=accept,
    )

    textfield.buffer.validate_while_typing = validate_while_typing

    dialog = Dialog(
        title=title,
        body=HSplit(
            [
                Label(text=text, dont_extend_height=True),
                textfield,
                CustomValidationToolbar(textfield.buffer),
            ],
            padding=D(preferred=1, max=1),
        ),
        buttons=[ok_button, cancel_button],
        with_background=True,
    )

    return _create_app(dialog, style)

You can call input_dialog just as you normally would.

result = input_dialog(title='Input dialog example', text='Please type your name:').run()

The only difference is you can also now pass in the validate_while_typing argument as well

result = input_dialog(title='Input dialog example',
                      text='Please type your favorite number:',
                      validator=NumberValidator(),
                      validate_while_typing=True
).run()
Megver83 commented 1 year ago

The problem, as mentioned here, is that validation is ran when pressing enter, but not the OK button.