Skip to content

Commit

Permalink
Merge branch 'main' into notification-border-right-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
darrenburns authored Sep 11, 2024
2 parents 0664f3a + 5867dcf commit ac8f46e
Show file tree
Hide file tree
Showing 24 changed files with 2,229 additions and 138 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Added `MaskedInput` widget https://github.com/Textualize/textual/pull/4783
- Input validation for floats and integers accept embedded underscores, e.g., "1_234_567" is valid. https://github.com/Textualize/textual/pull/4784

### Changed

- Input validation for integers no longer accepts scientific notation like '1.5e2'; must be castable to int. https://github.com/Textualize/textual/pull/4784
- Removed border-right from `Toast` https://github.com/Textualize/textual/pull/4984
- Some fixes in `RichLog` result in slightly different semantics, see docstrings for details https://github.com/Textualize/textual/pull/4978

### Fixed

- Input validation of floats no longer accepts NaN (not a number). https://github.com/Textualize/textual/pull/4784
- Fixed issues with screenshots by simplifying segments only for snapshot tests https://github.com/Textualize/textual/issues/4929
- Fixed `RichLog.write` not respecting `width` parameter https://github.com/Textualize/textual/pull/4978
- Fixed `RichLog` writing at wrong width when `write` occurs before width is known (e.g. in `compose` or `on_mount`) https://github.com/Textualize/textual/pull/4978
- Fixed `RichLog.write` incorrectly shrinking width to `RichLog.min_width` when `shrink=True` (now shrinks to fit content area instead) https://github.com/Textualize/textual/pull/4978

## [0.79.1] - 2024-08-31

Expand Down Expand Up @@ -108,6 +113,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- Fixed issue with Enter events causing unresponsive UI https://github.com/Textualize/textual/pull/4833


## [0.75.0] - 2024-08-01

### Added
Expand Down
32 changes: 32 additions & 0 deletions docs/examples/widgets/masked_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from textual.app import App, ComposeResult
from textual.widgets import Label, MaskedInput


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

def compose(self) -> ComposeResult:
yield Label("Enter a valid credit card number.")
yield MaskedInput(
template="9999-9999-9999-9999;0", # (2)!
)


app = MaskedInputApp()

if __name__ == "__main__":
app.run()
11 changes: 9 additions & 2 deletions docs/guide/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ Your test code will help you find bugs early, and alert you if you accidentally

## Testing frameworks for Textual

Textual doesn't require any particular test framework.
You can use any test framework you are familiar with, but we will be using [pytest](https://docs.pytest.org/) in this chapter.
Textual is an async framework powered by Python's [asyncio](https://docs.python.org/3/library/asyncio.html) library.
While Textual doesn't require a particular test framework, it must provide support for asyncio testing.

You can use any test framework you are familiar with, but we will be using [pytest](https://docs.pytest.org/)
along with the [pytest-asyncio](https://pytest-asyncio.readthedocs.io/) plugin in this chapter.

By default, the `pytest-asyncio` plugin requires each async test to be decorated with `@pytest.mark.asyncio`.
You can avoid having to add this marker to every async test
by setting `asyncio_mode = auto` in your pytest configuration
or by running pytest with the `--asyncio-mode=auto` option.

## Testing apps

Expand Down
10 changes: 10 additions & 0 deletions docs/widget_gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ Display a markdown document.
```{.textual path="docs/examples/widgets/markdown.py"}
```

## MaskedInput

A control to enter input according to a template mask.

[MaskedInput reference](./widgets/masked_input.md){ .md-button .md-button--primary }


```{.textual path="docs/examples/widgets/masked_input.py"}
```

## OptionList

Display a vertical list of options (options may be Rich renderables).
Expand Down
84 changes: 84 additions & 0 deletions docs/widgets/masked_input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# MaskedInput

!!! tip "Added in version 0.80.0"

A masked input derived from `Input`, allowing to restrict user input and give visual aid via a simple template mask, which also acts as an implicit *[validator][textual.validation.Validator]*.

- [x] Focusable
- [ ] Container

## Example

The example below shows a masked input to ease entering a credit card number.

=== "Output"

```{.textual path="docs/examples/widgets/masked_input.py"}
```

=== "checkbox.py"

```python
--8<-- "docs/examples/widgets/masked_input.py"
```

## Reactive Attributes

| Name | Type | Default | Description |
| ---------- | ----- | ------- | ------------------------- |
| `template` | `str` | `""` | The template mask string. |

### The template string format

A `MaskedInput` template length defines the maximum length of the input value. Each character of the mask defines a regular expression used to restrict what the user can insert in the corresponding position, and whether the presence of the character in the user input is required for the `MaskedInput` value to be considered valid, according to the following table:

| Mask character | Regular expression | Required? |
| -------------- | ------------------ | --------- |
| `A` | `[A-Za-z]` | Yes |
| `a` | `[A-Za-z]` | No |
| `N` | `[A-Za-z0-9]` | Yes |
| `n` | `[A-Za-z0-9]` | No |
| `X` | `[^ ]` | Yes |
| `x` | `[^ ]` | No |
| `9` | `[0-9]` | Yes |
| `0` | `[0-9]` | No |
| `D` | `[1-9]` | Yes |
| `d` | `[1-9]` | No |
| `#` | `[0-9+\-]` | No |
| `H` | `[A-Fa-f0-9]` | Yes |
| `h` | `[A-Fa-f0-9]` | No |
| `B` | `[0-1]` | Yes |
| `b` | `[0-1]` | No |

There are some special characters that can be used to control automatic case conversion during user input: `>` converts all subsequent user input to uppercase; `<` to lowercase; `!` disables automatic case conversion. Any other character that appears in the template mask is assumed to be a separator, which is a character that is automatically inserted when user reaches its position. All mask characters can be escaped by placing `\` in front of them, allowing any character to be used as separator.
The mask can be terminated by `;c`, where `c` is any character you want to be used as placeholder character. The `placeholder` parameter inherited by `Input` can be used to override this allowing finer grain tuning of the placeholder string.

## Messages

- [MaskedInput.Changed][textual.widgets.MaskedInput.Changed]
- [MaskedInput.Submitted][textual.widgets.MaskedInput.Submitted]

## Bindings

The masked input widget defines the following bindings:

::: textual.widgets.MaskedInput.BINDINGS
options:
show_root_heading: false
show_root_toc_entry: false

## Component Classes

The masked input widget provides the following component classes:

::: textual.widgets.MaskedInput.COMPONENT_CLASSES
options:
show_root_heading: false
show_root_toc_entry: false

---


::: textual.widgets.MaskedInput
options:
heading_level: 2
1 change: 1 addition & 0 deletions mkdocs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ nav:
- "widgets/log.md"
- "widgets/markdown_viewer.md"
- "widgets/markdown.md"
- "widgets/masked_input.md"
- "widgets/option_list.md"
- "widgets/placeholder.md"
- "widgets/pretty.md"
Expand Down
5 changes: 5 additions & 0 deletions src/textual/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,8 @@ def _get_textual_animations() -> AnimationLevel:

ESCAPE_DELAY: Final[float] = _get_environ_int("ESCDELAY", 100) / 1000.0
"""The delay (in seconds) before reporting an escape key (not used if the extend key protocol is available)."""

SLOW_THRESHOLD: int = _get_environ_int("TEXTUAL_SLOW_THRESHOLD", 500)
"""The time threshold (in milliseconds) after which a warning is logged
if message processing exceeds this duration.
"""
13 changes: 13 additions & 0 deletions src/textual/message_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
from __future__ import annotations

import asyncio
import os
import threading
from asyncio import CancelledError, Queue, QueueEmpty, Task, create_task
from contextlib import contextmanager
from functools import partial
from time import perf_counter
from typing import (
TYPE_CHECKING,
Any,
Expand All @@ -35,6 +37,7 @@
from ._context import prevent_message_types_stack
from ._on import OnNoWidget
from ._time import time
from .constants import SLOW_THRESHOLD
from .css.match import match
from .events import Event
from .message import Message
Expand Down Expand Up @@ -666,6 +669,16 @@ async def _dispatch_message(self, message: Message) -> None:
# Allow apps to treat events and messages separately
if isinstance(message, Event):
await self.on_event(message)
elif "debug" in self.app.features:
start = perf_counter()
await self._on_message(message)
if perf_counter() - start > SLOW_THRESHOLD / 1000:
log.warning(
f"method=<{self.__class__.__name__}."
f"{message.handler_name}>",
f"Took over {SLOW_THRESHOLD}ms to process.",
"\nTo avoid screen freezes, consider using a worker.",
)
else:
await self._on_message(message)
if self._next_callbacks:
Expand Down
2 changes: 2 additions & 0 deletions src/textual/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from ._loading_indicator import LoadingIndicator
from ._log import Log
from ._markdown import Markdown, MarkdownViewer
from ._masked_input import MaskedInput
from ._option_list import OptionList
from ._placeholder import Placeholder
from ._pretty import Pretty
Expand Down Expand Up @@ -68,6 +69,7 @@
"Log",
"Markdown",
"MarkdownViewer",
"MaskedInput",
"OptionList",
"Placeholder",
"Pretty",
Expand Down
Loading

0 comments on commit ac8f46e

Please sign in to comment.