Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validation #2600

Merged
merged 26 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7ca23de
A few different types of validation
darrenburns May 17, 2023
19ad365
Rename
darrenburns May 17, 2023
26371b1
Fix test
darrenburns May 17, 2023
63135cb
Updating validation framework
darrenburns May 18, 2023
3fbf681
Merge branch 'main' of github.com:willmcgugan/textual into validation
darrenburns May 18, 2023
96dd65c
Update lockfile
darrenburns May 18, 2023
0245813
Ensure validators can be None
darrenburns May 18, 2023
43882fe
Reworking the API a little
darrenburns May 22, 2023
dbab8fc
Convert Input.Changed to dataclass
darrenburns May 22, 2023
6333052
Add utility for getting failures as strings
darrenburns May 22, 2023
93480dc
Merge branch 'main' of github.com:Textualize/textual into validation
darrenburns May 22, 2023
7a14fc5
Update an example in Validator docstring
darrenburns May 22, 2023
05017d1
Remove some redundant `pass`es
darrenburns May 22, 2023
042a86d
Renaming variables
darrenburns May 22, 2023
828f529
Validating Input on submit, attaching result to Submitted event
darrenburns May 23, 2023
7b7c7e8
Testing various validation features
darrenburns May 23, 2023
862da2a
Merge branch 'main' of github.com:Textualize/textual into validation
darrenburns May 23, 2023
3665af3
Update snapshots and deps
darrenburns May 23, 2023
386de4e
Styling unfocused -invalid Input differently
darrenburns May 23, 2023
155d6e9
Add snapshot test around input validation and associated styles
darrenburns May 23, 2023
da271b7
Validation docs
darrenburns May 23, 2023
bd1a61a
Tidying validation docs in Input widget reference
darrenburns May 23, 2023
ab1580c
Fix mypy issues
darrenburns May 23, 2023
fdfa546
Remove __bool__ from Failure, make validator field required
darrenburns May 24, 2023
f22a5d6
Code review changes
darrenburns May 24, 2023
62df940
Improving error messages in Validators
darrenburns May 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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