mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Invert logic to specify events for input validation.
Related review comment: https://github.com/Textualize/textual/pull/3193#discussion_r1321250339.
This commit is contained in:
@@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
- TCSS styles `layer` and `layers` can be strings https://github.com/Textualize/textual/pull/3169
|
||||
- `Input` is now validated when focus moves out of it https://github.com/Textualize/textual/pull/3193
|
||||
- Attribute `Input.prevent_validation_on` (and `__init__` parameter of the same name) to customise when validation occurs https://github.com/Textualize/textual/pull/3193
|
||||
- Attribute `Input.validate_on` (and `__init__` parameter of the same name) to customise when validation occurs https://github.com/Textualize/textual/pull/3193
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
||||
from rich.highlighter import Highlighter
|
||||
from rich.segment import Segment
|
||||
from rich.text import Text
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .. import events
|
||||
from .._segment_tools import line_crop
|
||||
@@ -21,6 +22,9 @@ from ..suggester import Suggester, SuggestionReady
|
||||
from ..validation import ValidationResult, Validator
|
||||
from ..widget import Widget
|
||||
|
||||
InputValidationOn = Literal["blur", "changed", "submitted"]
|
||||
"""Possible messages that trigger input validation."""
|
||||
|
||||
|
||||
class _InputRenderable:
|
||||
"""Render the input content."""
|
||||
@@ -221,7 +225,7 @@ class Input(Widget, can_focus=True):
|
||||
*,
|
||||
suggester: Suggester | None = None,
|
||||
validators: Validator | Iterable[Validator] | None = None,
|
||||
prevent_validation_on: Iterable[type[Message]] | None = None,
|
||||
validate_on: Iterable[InputValidationOn] | None = None,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
@@ -237,8 +241,8 @@ class Input(Widget, can_focus=True):
|
||||
suggester: [`Suggester`][textual.suggester.Suggester] associated with this
|
||||
input instance.
|
||||
validators: An iterable of validators that the Input value will be checked against.
|
||||
prevent_validation_on: Message types for which validation shouldn't occur.
|
||||
Validation occurs for input changes and submissions, as well as on blur events.
|
||||
validate_on: When does input validation happen? The default is to validate
|
||||
on input changes and submissions, as well as on blur events.
|
||||
name: Optional name for the input widget.
|
||||
id: Optional ID for the widget.
|
||||
classes: Optional initial classes for the widget.
|
||||
@@ -258,14 +262,16 @@ class Input(Widget, can_focus=True):
|
||||
self.validators = []
|
||||
else:
|
||||
self.validators = list(validators)
|
||||
self.prevent_validation_on: set[type[Message]] = set(
|
||||
prevent_validation_on or []
|
||||
) & {self.Changed, self.Submitted, Blur}
|
||||
"""Set with events to skip validation on.
|
||||
|
||||
Validation is only performed on blur, when input changes and when it's submitted.
|
||||
Including any of these types of messages in this set will skip validation on
|
||||
these message types.
|
||||
_possible_validate_on_values = {"blur", "changed", "submitted"}
|
||||
self.validate_on = (
|
||||
set(validate_on) & _possible_validate_on_values
|
||||
if validate_on is not None
|
||||
else _possible_validate_on_values
|
||||
)
|
||||
"""Set with event names to do input validation on.
|
||||
|
||||
Validation can only be performed on blur, on input changes and on input submission.
|
||||
|
||||
Example:
|
||||
This creates an `Input` widget that only gets validated when the value
|
||||
@@ -274,7 +280,7 @@ class Input(Widget, can_focus=True):
|
||||
```py
|
||||
from textual.events import Blur
|
||||
|
||||
input = Input(prevent_validation_on=[Blur, Input.Changed])
|
||||
input = Input(validate_on=["submitted"])
|
||||
```
|
||||
"""
|
||||
|
||||
@@ -329,9 +335,7 @@ class Input(Widget, can_focus=True):
|
||||
self.refresh(layout=True)
|
||||
|
||||
validation_result = (
|
||||
self.validate(value)
|
||||
if self.Changed not in self.prevent_validation_on
|
||||
else None
|
||||
self.validate(value) if "changed" in self.validate_on else None
|
||||
)
|
||||
self.post_message(self.Changed(self, value, validation_result))
|
||||
|
||||
@@ -414,7 +418,7 @@ class Input(Widget, can_focus=True):
|
||||
|
||||
def _on_blur(self, _: Blur) -> None:
|
||||
self.blink_timer.pause()
|
||||
if Blur not in self.prevent_validation_on:
|
||||
if "blur" in self.validate_on:
|
||||
self.validate(self.value)
|
||||
|
||||
def _on_focus(self, _: Focus) -> None:
|
||||
@@ -606,8 +610,6 @@ class Input(Widget, can_focus=True):
|
||||
Normally triggered by the user pressing Enter. This will also run any validators.
|
||||
"""
|
||||
validation_result = (
|
||||
self.validate(self.value)
|
||||
if self.Submitted not in self.prevent_validation_on
|
||||
else None
|
||||
self.validate(self.value) if "submitted" in self.validate_on else None
|
||||
)
|
||||
self.post_message(self.Submitted(self, self.value, validation_result))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from textual import on
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.events import Blur
|
||||
@@ -6,16 +8,16 @@ from textual.widgets import Input
|
||||
|
||||
|
||||
class InputApp(App):
|
||||
def __init__(self, prevent_validation_on=None):
|
||||
def __init__(self, validate_on=None):
|
||||
super().__init__()
|
||||
self.messages = []
|
||||
self.validator = Number(minimum=1, maximum=5)
|
||||
self.prevent_validation_on = prevent_validation_on or set()
|
||||
self.validate_on = validate_on
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Input(
|
||||
validators=self.validator,
|
||||
prevent_validation_on=self.prevent_validation_on,
|
||||
validate_on=self.validate_on,
|
||||
)
|
||||
|
||||
@on(Input.Changed)
|
||||
@@ -94,33 +96,112 @@ async def test_on_blur_triggers_validation():
|
||||
assert input.has_class("-valid")
|
||||
|
||||
|
||||
async def test_prevent_validation_on_changes():
|
||||
app = InputApp([Input.Changed])
|
||||
@pytest.mark.parametrize(
|
||||
"validate_on",
|
||||
[
|
||||
set(),
|
||||
{"blur"},
|
||||
{"submitted"},
|
||||
{"blur", "submitted"},
|
||||
{"fried", "garbage"},
|
||||
],
|
||||
)
|
||||
async def test_validation_on_changed_should_not_happen(validate_on):
|
||||
app = InputApp(validate_on)
|
||||
async with app.run_test() as pilot:
|
||||
# sanity checks
|
||||
assert len(app.messages) == 0
|
||||
app.query_one(Input).value = "3"
|
||||
await pilot.pause()
|
||||
assert len(app.messages) == 1
|
||||
assert app.messages[0].validation_result is None
|
||||
|
||||
|
||||
async def test_prevent_validation_on_submission():
|
||||
app = InputApp([Input.Submitted])
|
||||
async with app.run_test() as pilot:
|
||||
await app.query_one(Input).action_submit()
|
||||
await pilot.pause()
|
||||
assert len(app.messages) == 1
|
||||
assert app.messages[0].validation_result is None
|
||||
|
||||
|
||||
async def test_prevent_validation_on_blur():
|
||||
app = InputApp([Blur])
|
||||
async with app.run_test() as pilot:
|
||||
input = app.query_one(Input)
|
||||
input.focus()
|
||||
assert not input.has_class("-valid")
|
||||
assert not input.has_class("-invalid")
|
||||
|
||||
input.value = "3"
|
||||
await pilot.pause()
|
||||
input.remove_class("-valid")
|
||||
assert len(app.messages) == 1
|
||||
assert app.messages[-1].validation_result is None
|
||||
assert not input.has_class("-valid")
|
||||
assert not input.has_class("-invalid")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"validate_on",
|
||||
[
|
||||
set(),
|
||||
{"blur"},
|
||||
{"changed"},
|
||||
{"blur", "changed"},
|
||||
{"fried", "garbage"},
|
||||
],
|
||||
)
|
||||
async def test_validation_on_submitted_should_not_happen(validate_on):
|
||||
app = InputApp(validate_on)
|
||||
async with app.run_test() as pilot:
|
||||
# sanity checks
|
||||
assert len(app.messages) == 0
|
||||
input = app.query_one(Input)
|
||||
assert not input.has_class("-valid")
|
||||
assert not input.has_class("-invalid")
|
||||
|
||||
await input.action_submit()
|
||||
await pilot.pause()
|
||||
assert len(app.messages) == 1
|
||||
assert app.messages[-1].validation_result is None
|
||||
assert not input.has_class("-valid")
|
||||
assert not input.has_class("-invalid")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"validate_on",
|
||||
[
|
||||
set(),
|
||||
{"submitted"},
|
||||
{"changed"},
|
||||
{"submitted", "changed"},
|
||||
{"fried", "garbage"},
|
||||
],
|
||||
)
|
||||
async def test_validation_on_blur_should_not_happen_unless_specified(validate_on):
|
||||
app = InputApp(validate_on)
|
||||
async with app.run_test() as pilot:
|
||||
# sanity checks
|
||||
input = app.query_one(Input)
|
||||
assert not input.has_class("-valid")
|
||||
assert not input.has_class("-invalid")
|
||||
|
||||
input.focus()
|
||||
await pilot.pause()
|
||||
app.set_focus(None)
|
||||
await pilot.pause()
|
||||
assert not input.has_class("-valid")
|
||||
assert not input.has_class("-invalid")
|
||||
|
||||
|
||||
async def test_none_validate_on_means_all_validations_happen():
|
||||
app = InputApp(None)
|
||||
async with app.run_test() as pilot:
|
||||
assert len(app.messages) == 0 # sanity checks
|
||||
input = app.query_one(Input)
|
||||
assert not input.has_class("-valid")
|
||||
assert not input.has_class("-invalid")
|
||||
|
||||
input.value = "3"
|
||||
await pilot.pause()
|
||||
assert len(app.messages) == 1
|
||||
assert app.messages[-1].validation_result is not None
|
||||
assert input.has_class("-valid")
|
||||
|
||||
input.remove_class("-valid")
|
||||
|
||||
await input.action_submit()
|
||||
await pilot.pause()
|
||||
assert len(app.messages) == 2
|
||||
assert app.messages[-1].validation_result is not None
|
||||
assert input.has_class("-valid")
|
||||
|
||||
input.remove_class("-valid")
|
||||
|
||||
input.focus()
|
||||
await pilot.pause()
|
||||
app.set_focus(None)
|
||||
await pilot.pause()
|
||||
assert input.has_class("-valid")
|
||||
|
||||
Reference in New Issue
Block a user