Skip to content

Commit

Permalink
Validation (#2600)
Browse files Browse the repository at this point in the history
* A few different types of validation

* Rename

* Fix test

* Updating validation framework

* Update lockfile

* Ensure validators can be None

* Reworking the API a little

* Convert Input.Changed to dataclass

* Add utility for getting failures as strings

* Update an example in Validator docstring

* Remove some redundant `pass`es

* Renaming variables

* Validating Input on submit, attaching result to Submitted event

* Testing various validation features

* Update snapshots and deps

* Styling unfocused -invalid Input differently

* Add snapshot test around input validation and associated styles

* Validation docs

* Tidying validation docs in Input widget reference

* Fix mypy issues

* Remove __bool__ from Failure, make validator field required

* Code review changes

* Improving error messages in Validators
  • Loading branch information
darrenburns authored May 25, 2023
1 parent 5cb30b5 commit 62fcefb
Show file tree
Hide file tree
Showing 14 changed files with 2,419 additions and 1,225 deletions.
1 change: 1 addition & 0 deletions docs/api/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: textual.validation
72 changes: 72 additions & 0 deletions docs/examples/widgets/input_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from textual import on
from textual.app import App, ComposeResult
from textual.validation import Function, Number, ValidationResult, Validator
from textual.widgets import Input, Label, Pretty


class InputApp(App):
# (6)!
CSS = """
Input.-valid {
border: tall $success 60%;
}
Input.-valid:focus {
border: tall $success;
}
Input {
margin: 1 1;
}
Label {
margin: 1 2;
}
Pretty {
margin: 1 2;
}
"""

def compose(self) -> ComposeResult:
yield Label("Enter an even number between 1 and 100 that is also a palindrome.")
yield Input(
placeholder="Enter a number...",
validators=[
Number(minimum=1, maximum=100), # (1)!
Function(is_even, "Value is not even."), # (2)!
Palindrome(), # (3)!
],
)
yield Pretty([])

@on(Input.Changed)
def show_invalid_reasons(self, event: Input.Changed) -> None:
# Updating the UI to show the reasons why validation failed
if not event.validation_result.is_valid: # (4)!
self.query_one(Pretty).update(event.validation_result.failure_descriptions)
else:
self.query_one(Pretty).update([])


def is_even(value: str) -> bool:
try:
return int(value) % 2 == 0
except ValueError:
return False


# A custom validator
class Palindrome(Validator): # (5)!
def validate(self, value: str) -> ValidationResult:
"""Check a string is equal to its reverse."""
if self.is_palindrome(value):
return self.success()
else:
return self.failure("That's not a palindrome :/")

@staticmethod
def is_palindrome(value: str) -> bool:
return value == value[::-1]


app = InputApp()

if __name__ == "__main__":
app.run()
48 changes: 46 additions & 2 deletions docs/widgets/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ A single-line text input widget.
- [x] Focusable
- [ ] Container

## Example
## Examples

### A Simple Example

The example below shows how you might create a simple form using two `Input` widgets.

Expand All @@ -20,10 +22,52 @@ The example below shows how you might create a simple form using two `Input` wid
--8<-- "docs/examples/widgets/input.py"
```

### Validating Input

You can supply one or more *[validators][textual.validation.Validator]* to the `Input` widget to validate the value.

When the value changes or the `Input` is submitted, all the supplied validators will run.

Validation is considered to have failed if *any* of the validators fail.

You can check whether the validation succeeded or failed inside an [Input.Changed][textual.widgets.Input.Changed] or
[Input.Submitted][textual.widgets.Input.Submitted] handler by looking at the `validation_result` attribute on these events.

In the example below, we show how to combine multiple validators and update the UI to tell the user
why validation failed.
Click the tabs to see the output for validation failures and successes.

=== "input_validation.py"

```python hl_lines="8-15 31-35 42-45 56-62"
--8<-- "docs/examples/widgets/input_validation.py"
```

1. `Number` is a built-in `Validator`. It checks that the value in the `Input` is a valid number, and optionally can check that it falls within a range.
2. `Function` lets you quickly define custom validation constraints. In this case, we check the value in the `Input` is even.
3. `Palindrome` is a custom `Validator` defined below.
4. The `Input.Changed` event has a `validation_result` attribute which contains information about the validation that occurred when the value changed.
5. Here's how we can implement a custom validator which checks if a string is a palindrome. Note how the description passed into `self.failure` corresponds to the message seen on UI.
6. Textual offers default styling for the `-invalid` CSS class (a red border), which is automatically applied to `Input` when validation fails. We can also provide custom styling for the `-valid` class, as seen here. In this case, we add a green border around the `Input` to indicate successful validation.

=== "Validation Failure"

```{.textual path="docs/examples/widgets/input_validation.py" press="-,2,3"}
```

=== "Validation Success"

```{.textual path="docs/examples/widgets/input_validation.py" press="4,4"}
```

Textual offers several [built-in validators][textual.validation] for common requirements,
but you can easily roll your own by extending [Validator][textual.validation.Validator],
as seen for `Palindrome` in the example above.

## Reactive Attributes

| Name | Type | Default | Description |
| ----------------- | ------ | ------- | --------------------------------------------------------------- |
|-------------------|--------|---------|-----------------------------------------------------------------|
| `cursor_blink` | `bool` | `True` | True if cursor blinking is enabled. |
| `value` | `str` | `""` | The value currently in the text input. |
| `cursor_position` | `int` | `0` | The index of the cursor in the value string. |
Expand Down
Loading

0 comments on commit 62fcefb

Please sign in to comment.