Merge branch 'main' into unfootgun-worker-thread

This commit is contained in:
Dave Pearson
2023-07-17 16:23:39 +01:00
26 changed files with 1857 additions and 777 deletions

View File

@@ -5,15 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
## [0.30.0] - 2023-07-17
### Added
- Added `DataTable.remove_column` method https://github.com/Textualize/textual/pull/2899
- Added notifications https://github.com/Textualize/textual/pull/2866
- Added `on_complete` callback to scroll methods https://github.com/Textualize/textual/pull/2903
### Fixed
- Fixed CancelledError issue with timer https://github.com/Textualize/textual/issues/2854
- Fixed Toggle Buttons issue with not being clickable/hoverable https://github.com/Textualize/textual/pull/2930
### Changed
@@ -1119,6 +1122,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling
[0.30.0]: https://github.com/Textualize/textual/compare/v0.29.0...v0.30.0
[0.29.0]: https://github.com/Textualize/textual/compare/v0.28.1...v0.29.0
[0.28.1]: https://github.com/Textualize/textual/compare/v0.28.0...v0.28.1
[0.28.0]: https://github.com/Textualize/textual/compare/v0.27.0...v0.28.0

View File

@@ -0,0 +1,80 @@
---
draft: false
date: 2023-07-17
categories:
- Release
title: "Textual 0.30.0 adds desktop-style notifications"
authors:
- willmcgugan
---
# Textual 0.30.0 adds desktop-style notifications
We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.
<!-- more -->
By sheer coincidence we reached [20,000 stars on GitHub](https://github.com/Textualize/textual) today.
Now stars don't mean all that much (at least until we can spend them on coffee), but its nice to know that twenty thousand developers thought Textual was interesting enough to hit the ★ button.
Thank you!
In other news: we moved office.
We are now a stone's throw away from Edinburgh Castle.
The office is around three times as big as the old place, which means we have room for wide standup desks and dual monitors.
But more importantly we have room for new employees.
Don't send your CVs just yet, but we hope to grow the team before the end of the year.
Exciting times.
## New Release
And now, for the main feature.
Version 0.30 adds a new notification system.
Similar to desktop notifications, it displays a small window with a title and message (called a *toast*) for a pre-defined number of seconds.
Notifications are great for short timely messages to add supplementary information for the user.
Here it is in action:
<div class="video-wrapper">
<iframe
width="560" height="315"
src="https://www.youtube.com/embed/HIHRefjfcVc"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
The API is super simple.
To display a notification, call `notify()` with a message and an optional title.
```python
def on_mount(self) -> None:
self.notify("Hello, from Textual!", title="Welcome")
```
## Textualize Video Channel
In case you missed it; Textualize now has a [YouTube](https://www.youtube.com/channel/UCo4nHAZv_cIlAiCSP2IyiOA) channel.
Our very own [Rodrigo](https://twitter.com/mathsppblog) has recorded a video tutorial series on how to build Textual apps.
Check it out!
<div class="video-wrapper">
<iframe
width="560" height="315"
src="https://www.youtube.com/embed/kpOBRI56GXM"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
We will be adding more videos in the near future, covering anything from beginner to advanced topics.
Don't worry if you prefer reading to watching videos.
We will be adding plenty more content to the [Textual docs](https://textual.textualize.io/) in the near future.
Watch this space.
As always, if you want to discuss anything with the Textual developers, join us on the [Discord server](https://discord.gg/Enf6Z3qhVr).

View File

@@ -0,0 +1,26 @@
from textual.app import App
class ToastApp(App[None]):
def on_mount(self) -> None:
# Show an information notification.
self.notify("It's an older code, sir, but it checks out.")
# Show a warning. Note that Textual's notification system allows
# for the use of Rich console markup.
self.notify(
"Now witness the firepower of this fully "
"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!",
title="Possible trap detected",
severity="warning",
)
# Show an error. Set a longer timeout so it's noticed.
self.notify("It's a trap!", severity="error", timeout=10)
# Show an information notification, but without any sort of title.
self.notify("It's against my programming to impersonate a deity.", title="")
if __name__ == "__main__":
ToastApp().run()

View File

@@ -42,7 +42,7 @@ Then, we set `max-width` individually on each placeholder.
max-width: 10;
/* Set the maximum width to 25% of the viewport width */
max-width: 25vh;
max-width: 25vw;
```
## Python
@@ -52,7 +52,7 @@ max-width: 25vh;
widget.styles.max_width = 10
# Set the maximum width to 25% of the viewport width
widget.styles.max_width = "25vh"
widget.styles.max_width = "25vw"
```
## See also

View File

@@ -42,7 +42,7 @@ Then, we set `min-width` individually on each placeholder.
min-width: 10;
/* Set the minimum width to 25% of the viewport width */
min-width: 25vh;
min-width: 25vw;
```
## Python
@@ -52,7 +52,7 @@ min-width: 25vh;
widget.styles.min_width = 10
# Set the minimum width to 25% of the viewport width
widget.styles.min_width = "25vh"
widget.styles.min_width = "25vw"
```
## See also

79
docs/widgets/toast.md Normal file
View File

@@ -0,0 +1,79 @@
# Toast
!!! tip "Added in version 0.30.0"
A widget which displays a notification message.
- [ ] Focusable
- [ ] Container
Note that `Toast` isn't designed to be used directly in your applications,
but it is instead used by [`notify`][textual.app.App.notify] to
display a message when using Textual's built-in notification system.
## Styling
You can customize the style of Toasts by targeting the `Toast` [CSS type](/guide/CSS/#type-selector).
For example:
```scss
Toast {
padding: 3;
}
```
The three severity levels also have corresponding
[classes](/guide/CSS/#class-name-selector), allowing you to target the
different styles of notification. They are:
- `-information`
- `-warning`
- `-error`
If you wish to tailor the notifications for your application you can add
rules to your CSS like this:
```scss
Toast.-information {
/* Styling here. */
}
Toast.-warning {
/* Styling here. */
}
Toast.-error {
/* Styling here. */
}
```
You can customize just the title wih the `toast--title` class.
The following would make the title italic for an information toast:
```scss
Toast.-information .toast--title {
text-style: italic;
}
```
## Example
=== "Output"
```{.textual path="docs/examples/widgets/toast.py"}
```
=== "toast.py"
```python
--8<-- "docs/examples/widgets/toast.py"
```
---
::: textual.widgets._toast
options:
show_root_heading: true
show_root_toc_entry: true

283
poetry.lock generated
View File

@@ -128,14 +128,14 @@ frozenlist = ">=1.1.0"
[[package]]
name = "anyio"
version = "3.7.0"
version = "3.7.1"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0"},
{file = "anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce"},
{file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
{file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
]
[package.dependencies]
@@ -145,7 +145,7 @@ sniffio = ">=1.1"
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
doc = ["Sphinx (>=6.1.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "sphinxcontrib-jquery"]
doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (<0.22)"]
@@ -287,99 +287,99 @@ files = [
[[package]]
name = "charset-normalizer"
version = "3.1.0"
version = "3.2.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
{file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
{file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
{file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"},
{file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"},
{file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"},
{file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"},
{file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"},
{file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"},
{file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"},
{file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"},
{file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"},
{file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"},
{file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"},
{file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"},
{file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"},
{file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"},
{file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"},
{file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"},
{file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"},
{file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"},
{file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"},
{file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"},
{file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"},
{file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"},
{file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"},
{file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"},
{file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"},
{file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"},
{file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
{file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
{file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
{file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
{file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
{file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
{file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
{file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
{file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
{file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
{file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
{file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
]
[[package]]
name = "click"
version = "8.1.3"
version = "8.1.5"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
{file = "click-8.1.5-py3-none-any.whl", hash = "sha256:e576aa487d679441d7d30abb87e1b43d24fc53bffb8758443b1a9e1cee504548"},
{file = "click-8.1.5.tar.gz", hash = "sha256:4be4b1af8d665c6d942909916d31a213a106800c47d0eeba73d34da3cbc11367"},
]
[package.dependencies]
@@ -484,14 +484,14 @@ toml = ["tomli"]
[[package]]
name = "distlib"
version = "0.3.6"
version = "0.3.7"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
{file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
{file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
]
[[package]]
@@ -644,14 +644,14 @@ smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
version = "3.1.31"
version = "3.1.32"
description = "GitPython is a Python library used to interact with Git repositories"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"},
{file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"},
{file = "GitPython-3.1.32-py3-none-any.whl", hash = "sha256:e3d59b1c2c6ebb9dfa7a184daf3b6dd4914237e7488a1730a6d8f6f5d0b4187f"},
{file = "GitPython-3.1.32.tar.gz", hash = "sha256:8d9b8cb1e80b9735e8717c9362079d3ce4c6e5ddeebedd0361b228c3a67a62f6"},
]
[package.dependencies]
@@ -1401,14 +1401,14 @@ files = [
[[package]]
name = "platformdirs"
version = "3.8.0"
version = "3.9.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"},
{file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"},
{file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"},
{file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"},
]
[package.dependencies]
@@ -1474,14 +1474,14 @@ plugins = ["importlib-metadata"]
[[package]]
name = "pymdown-extensions"
version = "10.0.1"
version = "10.1"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pymdown_extensions-10.0.1-py3-none-any.whl", hash = "sha256:ae66d84013c5d027ce055693e09a4628b67e9dec5bce05727e45b0918e36f274"},
{file = "pymdown_extensions-10.0.1.tar.gz", hash = "sha256:b44e1093a43b8a975eae17b03c3a77aad4681b3b56fce60ce746dbef1944c8cb"},
{file = "pymdown_extensions-10.1-py3-none-any.whl", hash = "sha256:ef25dbbae530e8f67575d222b75ff0649b1e841e22c2ae9a20bad9472c2207dc"},
{file = "pymdown_extensions-10.1.tar.gz", hash = "sha256:508009b211373058debb8247e168de4cbcb91b1bff7b5e961b2c3e864e00b195"},
]
[package.dependencies]
@@ -1534,14 +1534,14 @@ testing = ["coverage (==6.2)", "mypy (==0.931)"]
[[package]]
name = "pytest-asyncio"
version = "0.21.0"
version = "0.21.1"
description = "Pytest support for asyncio"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-asyncio-0.21.0.tar.gz", hash = "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b"},
{file = "pytest_asyncio-0.21.0-py3-none-any.whl", hash = "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c"},
{file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
{file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
]
[package.dependencies]
@@ -1574,14 +1574,14 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
[[package]]
name = "pytest-textual-snapshot"
version = "0.1.0"
version = "0.2.0"
description = "Snapshot testing for Textual apps"
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
files = [
{file = "pytest_textual_snapshot-0.1.0-py3-none-any.whl", hash = "sha256:7310002ed152ce6cc654fff7f83ec88eecc36116c5bf7995decc0a6424809da3"},
{file = "pytest_textual_snapshot-0.1.0.tar.gz", hash = "sha256:5e20629f2413a3689a485117e709e6d4010b7b5b558c0070414899b9768697f0"},
{file = "pytest_textual_snapshot-0.2.0-py3-none-any.whl", hash = "sha256:663fe07bf62181ec0c63139daaeaf50eb8088164037eb30d721f028adc9edc8c"},
{file = "pytest_textual_snapshot-0.2.0.tar.gz", hash = "sha256:5e9f8c4b1b011bdae67d4f1129530afd6611f3f8bcf03cf06699402179bc12cf"},
]
[package.dependencies]
@@ -1912,21 +1912,21 @@ pytest = ">=5.1.0,<8.0.0"
[[package]]
name = "textual-dev"
version = "1.0.0"
version = "1.0.1"
description = "Development tools for working with Textual"
category = "dev"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "textual_dev-1.0.0-py3-none-any.whl", hash = "sha256:af6ca102bd46cf5115a108329cdfe1c7aec26cab81e83fd78d884648ad84e23b"},
{file = "textual_dev-1.0.0.tar.gz", hash = "sha256:1aa32f58976eb63078d2b8454e2d40a7806d7b205e3ee89e34ca2941548dbbd8"},
{file = "textual_dev-1.0.1-py3-none-any.whl", hash = "sha256:419fc426c120f04f89ab0cb1aa88f7873dd7cdb9c21618e709175c8eaff6b566"},
{file = "textual_dev-1.0.1.tar.gz", hash = "sha256:9f4c40655cbb56af7ee92805ef14fa24ae98ff8b0ae778c59de7222f1caa7281"},
]
[package.dependencies]
aiohttp = ">=3.8.1"
click = ">=8.1.2"
msgpack = ">=1.0.3"
textual = "*"
textual = ">=0.29.0"
typing-extensions = ">=4.4.0,<5.0.0"
[[package]]
@@ -2022,36 +2022,53 @@ files = [
[[package]]
name = "typed-ast"
version = "1.5.4"
version = "1.5.5"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
{file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
{file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
{file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
{file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
{file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
{file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
{file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
{file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
{file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
{file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
{file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"},
{file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"},
{file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"},
{file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"},
{file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"},
{file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"},
{file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"},
{file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"},
{file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"},
{file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"},
{file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"},
{file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"},
{file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"},
{file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"},
{file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"},
{file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"},
{file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"},
{file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"},
{file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"},
{file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"},
{file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"},
{file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"},
{file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"},
{file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"},
{file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"},
{file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"},
{file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"},
{file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"},
{file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"},
{file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"},
{file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"},
{file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"},
{file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"},
{file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"},
{file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"},
{file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"},
{file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"},
{file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"},
{file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"},
{file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"},
{file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"},
]
[[package]]
@@ -2125,14 +2142,14 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "virtualenv"
version = "20.23.1"
version = "20.24.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.23.1-py3-none-any.whl", hash = "sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419"},
{file = "virtualenv-20.23.1.tar.gz", hash = "sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1"},
{file = "virtualenv-20.24.0-py3-none-any.whl", hash = "sha256:18d1b37fc75cc2670625702d76849a91ebd383768b4e91382a8d51be3246049e"},
{file = "virtualenv-20.24.0.tar.gz", hash = "sha256:e2a7cef9da880d693b933db7654367754f14e20650dc60e8ee7385571f8593a3"},
]
[package.dependencies]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "0.29.0"
version = "0.30.0"
homepage = "https://github.com/Textualize/textual"
description = "Modern Text User Interface framework"
authors = ["Will McGugan <will@textualize.io>"]

View File

@@ -90,10 +90,12 @@ from .keys import (
_get_unicode_name_from_key,
)
from .messages import CallbackType
from .notifications import Notification, Notifications, SeverityLevel
from .reactive import Reactive
from .renderables.blank import Blank
from .screen import Screen, ScreenResultCallbackType, ScreenResultType
from .widget import AwaitMount, Widget
from .widgets._toast import ToastRack
if TYPE_CHECKING:
from textual_dev.client import DevtoolsClient
@@ -443,6 +445,7 @@ class App(Generic[ReturnType], DOMNode):
self._return_value: ReturnType | None = None
self._exit = False
self._disable_tooltips = False
self._disable_notifications = False
self.css_monitor = (
FileMonitor(self.css_path, self._on_css_change)
@@ -455,6 +458,7 @@ class App(Generic[ReturnType], DOMNode):
self._batch_count = 0
self.set_class(self.dark, "-dark-mode")
self.set_class(not self.dark, "-light-mode")
self._notifications = Notifications()
def validate_title(self, title: Any) -> str:
"""Make sure the title is set to a string."""
@@ -1029,6 +1033,7 @@ class App(Generic[ReturnType], DOMNode):
headless: bool = True,
size: tuple[int, int] | None = (80, 24),
tooltips: bool = False,
notifications: bool = False,
message_hook: Callable[[Message], None] | None = None,
) -> AsyncGenerator[Pilot, None]:
"""An asynchronous context manager for testing app.
@@ -1048,12 +1053,14 @@ class App(Generic[ReturnType], DOMNode):
size: Force terminal size to `(WIDTH, HEIGHT)`,
or None to auto-detect.
tooltips: Enable tooltips when testing.
notifications: Enable notifications when testing.
message_hook: An optional callback that will called with every message going through the app.
"""
from .pilot import Pilot
app = self
app._disable_tooltips = not tooltips
app._disable_notifications = not notifications
app_ready_event = asyncio.Event()
def on_app_ready() -> None:
@@ -2769,3 +2776,94 @@ class App(Generic[ReturnType], DOMNode):
def _end_update(self) -> None:
if self._sync_available and self._driver is not None:
self._driver.write(SYNC_END)
def _refresh_notifications(self) -> None:
"""Refresh the notifications on the current screen, if one is available."""
# If we've got a screen to hand...
if self.screen is not None:
try:
# ...see if it has a toast rack.
toast_rack = self.screen.get_child_by_type(ToastRack)
except NoMatches:
# It doesn't. That's fine. Either there won't ever be one,
# or one will turn up. Things will work out later.
return
# Update the toast rack.
toast_rack.show(self._notifications)
def notify(
self,
message: str,
*,
title: str | None = None,
severity: SeverityLevel = "information",
timeout: float = Notification.timeout,
) -> Notification:
"""Create a notification.
Args:
message: The message for the notification.
title: The title for the notification.
severity: The severity of the notification.
timeout: The timeout for the notification.
Returns:
The new notification.
The `notify` method is used to create an application-wide
notification, shown in a [`Toast`][textual.widgets._toast.Toast],
normally originating in the bottom right corner of the display.
Notifications can have the following severity levels:
- `information`
- `warning`
- `error`
The default is `information`.
If no `title` is provided, the title of the notification will
reflect the severity. If you wish to create a notification that has
no title whatsoever, pass an empty title (`""`).
Example:
```python
# Show an information notification.
self.notify("It's an older code, sir, but it checks out.")
# Show a warning. Note that Textual's notification system allows
# for the use of Rich console markup.
self.notify(
"Now witness the firepower of this fully "
"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!",
title="Possible trap detected",
severity="warning",
)
# Show an error. Set a longer timeout so it's noticed.
self.notify("It's a trap!", severity="error", timeout=10)
# Show an information notification, but without any sort of title.
self.notify("It's against my programming to impersonate a deity.", title="")
```
"""
notification = Notification(message, title, severity, timeout)
self._notifications.add(notification)
self._refresh_notifications()
return notification
def unnotify(self, notification: Notification, refresh: bool = True) -> None:
"""Remove a notification from the notification collection.
Args:
notification: The notification to remove.
refresh: Flag to say if the display of notifications should be refreshed.
"""
del self._notifications[notification]
if refresh:
self._refresh_notifications()
def clear_notifications(self) -> None:
"""Clear all the current notifications."""
self._notifications.clear()
self._refresh_notifications()

View File

@@ -0,0 +1,108 @@
"""Provides classes for holding and managing notifications."""
from __future__ import annotations
from dataclasses import dataclass, field
from time import time
from typing import Iterator
from uuid import uuid4
from rich.repr import Result
from typing_extensions import Literal, Self, TypeAlias
SeverityLevel: TypeAlias = Literal["information", "warning", "error"]
"""The severity level for a notification."""
@dataclass
class Notification:
"""Holds the details of a notification."""
message: str
"""The message for the notification."""
title: str | None = None
"""The title for the notification."""
severity: SeverityLevel = "information"
"""The severity level for the notification."""
timeout: float = 3
"""The timeout for the notification."""
raised_at: float = field(default_factory=time)
"""The time when the notification was raised (in Unix time)."""
identity: str = field(default_factory=lambda: str(uuid4()))
"""The unique identity of the notification."""
@property
def time_left(self) -> float:
"""The time left until this notification expires"""
return (self.raised_at + self.timeout) - time()
@property
def has_expired(self) -> bool:
"""Has the notification expired?"""
return self.time_left <= 0
def __rich_repr__(self) -> Result:
yield "message", self.message
yield "title", self.title, None
yield "severity", self.severity
yield "raised_it", self.raised_at
yield "identity", self.identity
yield "time_left", self.time_left
yield "has_expired", self.has_expired
class Notifications:
"""Class for managing a collection of notifications."""
def __init__(self) -> None:
"""Initialise the notification collection."""
self._notifications: dict[str, Notification] = {}
def _reap(self) -> Self:
"""Remove any expired notifications from the notification collection."""
for notification in list(self._notifications.values()):
if notification.has_expired:
del self._notifications[notification.identity]
return self
def add(self, notification: Notification) -> Self:
"""Add the given notification to the collection of managed notifications.
Args:
notification: The notification to add.
Returns:
Self.
"""
self._reap()._notifications[notification.identity] = notification
return self
def clear(self) -> Self:
"""Clear all the notifications."""
self._notifications.clear()
return self
def __len__(self) -> int:
"""The number of notifications."""
return len(self._reap()._notifications)
def __iter__(self) -> Iterator[Notification]:
return iter(self._reap()._notifications.values())
def __contains__(self, notification: Notification) -> bool:
return notification.identity in self._notifications
def __delitem__(self, notification: Notification) -> None:
try:
del self._reap()._notifications[notification.identity]
except KeyError:
# An attempt to remove a notification we don't know about is a
# no-op. What matters here is that the notification is forgotten
# about, and it looks like a caller has tried to be
# belt-and-braces. We're fine with this.
pass

View File

@@ -1,444 +0,0 @@
[![Downloads](https://pepy.tech/badge/rich/month)](https://pepy.tech/project/rich)
[![PyPI version](https://badge.fury.io/py/rich.svg)](https://badge.fury.io/py/rich)
[![codecov](https://codecov.io/gh/willmcgugan/rich/branch/master/graph/badge.svg)](https://codecov.io/gh/willmcgugan/rich)
[![Rich blog](https://img.shields.io/badge/blog-rich%20news-yellowgreen)](https://www.willmcgugan.com/tag/rich/)
[![Twitter Follow](https://img.shields.io/twitter/follow/willmcgugan.svg?style=social)](https://twitter.com/willmcgugan)
![Logo](https://github.com/willmcgugan/rich/raw/master/imgs/logo.svg)
[中文 readme](https://github.com/willmcgugan/rich/blob/master/README.cn.md) • [Lengua española readme](https://github.com/willmcgugan/rich/blob/master/README.es.md) • [Deutsche readme](https://github.com/willmcgugan/rich/blob/master/README.de.md) • [Läs på svenska](https://github.com/willmcgugan/rich/blob/master/README.sv.md) • [日本語 readme](https://github.com/willmcgugan/rich/blob/master/README.ja.md) • [한국어 readme](https://github.com/willmcgugan/rich/blob/master/README.kr.md)
Rich is a Python library for _rich_ text and beautiful formatting in the terminal.
The [Rich API](https://rich.readthedocs.io/en/latest/) makes it easy to add color and style to terminal output. Rich can also render pretty tables, progress bars, markdown, syntax highlighted source code, tracebacks, and more — out of the box.
![Features](https://github.com/willmcgugan/rich/raw/master/imgs/features.png)
For a video introduction to Rich see [calmcode.io](https://calmcode.io/rich/introduction.html) by [@fishnets88](https://twitter.com/fishnets88).
See what [people are saying about Rich](https://www.willmcgugan.com/blog/pages/post/rich-tweets/).
## Compatibility
Rich works with Linux, OSX, and Windows. True color / emoji works with new Windows Terminal, classic terminal is limited to 16 colors. Rich requires Python 3.6.1 or later.
Rich works with [Jupyter notebooks](https://jupyter.org/) with no additional configuration required.
## Installing
Install with `pip` or your favorite PyPi package manager.
```
pip install rich
```
Run the following to test Rich output on your terminal:
```
python -m rich
```
## Rich Print
To effortlessly add rich output to your application, you can import the [rich print](https://rich.readthedocs.io/en/latest/introduction.html#quick-start) method, which has the same signature as the builtin Python function. Try this:
```python
from rich import print
print("Hello, [bold magenta]World[/bold magenta]!", ":vampire:", locals())
```
![Hello World](https://github.com/willmcgugan/rich/raw/master/imgs/print.png)
## Rich REPL
Rich can be installed in the Python REPL, so that any data structures will be pretty printed and highlighted.
```python
>>> from rich import pretty
>>> pretty.install()
```
![REPL](https://github.com/willmcgugan/rich/raw/master/imgs/repl.png)
## Using the Console
For more control over rich terminal content, import and construct a [Console](https://rich.readthedocs.io/en/latest/reference/console.html#rich.console.Console) object.
```python
from rich.console import Console
console = Console()
```
The Console object has a `print` method which has an intentionally similar interface to the builtin `print` function. Here's an example of use:
```python
console.print("Hello", "World!")
```
As you might expect, this will print `"Hello World!"` to the terminal. Note that unlike the builtin `print` function, Rich will word-wrap your text to fit within the terminal width.
There are a few ways of adding color and style to your output. You can set a style for the entire output by adding a `style` keyword argument. Here's an example:
```python
console.print("Hello", "World!", style="bold red")
```
The output will be something like the following:
![Hello World](https://github.com/willmcgugan/rich/raw/master/imgs/hello_world.png)
That's fine for styling a line of text at a time. For more finely grained styling, Rich renders a special markup which is similar in syntax to [bbcode](https://en.wikipedia.org/wiki/BBCode). Here's an example:
```python
console.print("Where there is a [bold cyan]Will[/bold cyan] there [u]is[/u] a [i]way[/i].")
```
![Console Markup](https://github.com/willmcgugan/rich/raw/master/imgs/where_there_is_a_will.png)
You can use a Console object to generate sophisticated output with minimal effort. See the [Console API](https://rich.readthedocs.io/en/latest/console.html) docs for details.
## Rich Inspect
Rich has an [inspect](https://rich.readthedocs.io/en/latest/reference/init.html?highlight=inspect#rich.inspect) function which can produce a report on any Python object, such as class, instance, or builtin.
```python
>>> my_list = ["foo", "bar"]
>>> from rich import inspect
>>> inspect(my_list, methods=True)
```
![Log](https://github.com/willmcgugan/rich/raw/master/imgs/inspect.png)
See the [inspect docs](https://rich.readthedocs.io/en/latest/reference/init.html#rich.inspect) for details.
# Rich Library
Rich contains a number of builtin _renderables_ you can use to create elegant output in your CLI and help you debug your code.
Click the following headings for details:
<details>
<summary>Log</summary>
The Console object has a `log()` method which has a similar interface to `print()`, but also renders a column for the current time and the file and line which made the call. By default Rich will do syntax highlighting for Python structures and for repr strings. If you log a collection (i.e. a dict or a list) Rich will pretty print it so that it fits in the available space. Here's an example of some of these features.
```python
from rich.console import Console
console = Console()
test_data = [
{"jsonrpc": "2.0", "method": "sum", "params": [None, 1, 2, 4, False, True], "id": "1",},
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"},
]
def test_log():
enabled = False
context = {
"foo": "bar",
}
movies = ["Deadpool", "Rise of the Skywalker"]
console.log("Hello from", console, "!")
console.log(test_data, log_locals=True)
test_log()
```
The above produces the following output:
![Log](https://github.com/willmcgugan/rich/raw/master/imgs/log.png)
Note the `log_locals` argument, which outputs a table containing the local variables where the log method was called.
The log method could be used for logging to the terminal for long running applications such as servers, but is also a very nice debugging aid.
</details>
<details>
<summary>Logging Handler</summary>
You can also use the builtin [Handler class](https://rich.readthedocs.io/en/latest/logging.html) to format and colorize output from Python's logging module. Here's an example of the output:
![Logging](https://github.com/willmcgugan/rich/raw/master/imgs/logging.png)
</details>
<details>
<summary>Emoji</summary>
To insert an emoji in to console output place the name between two colons. Here's an example:
```python
>>> console.print(":smiley: :vampire: :pile_of_poo: :thumbs_up: :raccoon:")
😃 🧛 💩 👍 🦝
```
Please use this feature wisely.
</details>
<details>
<summary>Tables</summary>
Rich can render flexible [tables](https://rich.readthedocs.io/en/latest/tables.html) with unicode box characters. There is a large variety of formatting options for borders, styles, cell alignment etc.
![table movie](https://github.com/willmcgugan/rich/raw/master/imgs/table_movie.gif)
The animation above was generated with [table_movie.py](https://github.com/willmcgugan/rich/blob/master/examples/table_movie.py) in the examples directory.
Here's a simpler table example:
```python
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(show_header=True, header_style="bold magenta")
table.add_column("Date", style="dim", width=12)
table.add_column("Title")
table.add_column("Production Budget", justify="right")
table.add_column("Box Office", justify="right")
table.add_row(
"Dev 20, 2019", "Star Wars: The Rise of Skywalker", "$275,000,000", "$375,126,118"
)
table.add_row(
"May 25, 2018",
"[red]Solo[/red]: A Star Wars Story",
"$275,000,000",
"$393,151,347",
)
table.add_row(
"Dec 15, 2017",
"Star Wars Ep. VIII: The Last Jedi",
"$262,000,000",
"[bold]$1,332,539,889[/bold]",
)
console.print(table)
```
This produces the following output:
![table](https://github.com/willmcgugan/rich/raw/master/imgs/table.png)
Note that console markup is rendered in the same way as `print()` and `log()`. In fact, anything that is renderable by Rich may be included in the headers / rows (even other tables).
The `Table` class is smart enough to resize columns to fit the available width of the terminal, wrapping text as required. Here's the same example, with the terminal made smaller than the table above:
![table2](https://github.com/willmcgugan/rich/raw/master/imgs/table2.png)
</details>
<details>
<summary>Progress Bars</summary>
Rich can render multiple flicker-free [progress](https://rich.readthedocs.io/en/latest/progress.html) bars to track long-running tasks.
For basic usage, wrap any sequence in the `track` function and iterate over the result. Here's an example:
```python
from rich.progress import track
for step in track(range(100)):
do_step(step)
```
It's not much harder to add multiple progress bars. Here's an example taken from the docs:
![progress](https://github.com/willmcgugan/rich/raw/master/imgs/progress.gif)
The columns may be configured to show any details you want. Built-in columns include percentage complete, file size, file speed, and time remaining. Here's another example showing a download in progress:
![progress](https://github.com/willmcgugan/rich/raw/master/imgs/downloader.gif)
To try this out yourself, see [examples/downloader.py](https://github.com/willmcgugan/rich/blob/master/examples/downloader.py) which can download multiple URLs simultaneously while displaying progress.
</details>
<details>
<summary>Status</summary>
For situations where it is hard to calculate progress, you can use the [status](https://rich.readthedocs.io/en/latest/reference/console.html#rich.console.Console.status) method which will display a 'spinner' animation and message. The animation won't prevent you from using the console as normal. Here's an example:
```python
from time import sleep
from rich.console import Console
console = Console()
tasks = [f"task {n}" for n in range(1, 11)]
with console.status("[bold green]Working on tasks...") as status:
while tasks:
task = tasks.pop(0)
sleep(1)
console.log(f"{task} complete")
```
This generates the following output in the terminal.
![status](https://github.com/willmcgugan/rich/raw/master/imgs/status.gif)
The spinner animations were borrowed from [cli-spinners](https://www.npmjs.com/package/cli-spinners). You can select a spinner by specifying the `spinner` parameter. Run the following command to see the available values:
```
python -m rich.spinner
```
The above command generate the following output in the terminal:
![spinners](https://github.com/willmcgugan/rich/raw/master/imgs/spinners.gif)
</details>
<details>
<summary>Tree</summary>
Rich can render a [tree](https://rich.readthedocs.io/en/latest/tree.html) with guide lines. A tree is ideal for displaying a file structure, or any other hierarchical data.
The labels of the tree can be simple text or anything else Rich can render. Run the following for a demonstration:
```
python -m rich.tree
```
This generates the following output:
![markdown](https://github.com/willmcgugan/rich/raw/master/imgs/tree.png)
See the [tree.py](https://github.com/willmcgugan/rich/blob/master/examples/tree.py) example for a script that displays a tree view of any directory, similar to the linux `tree` command.
</details>
<details>
<summary>Columns</summary>
Rich can render content in neat [columns](https://rich.readthedocs.io/en/latest/columns.html) with equal or optimal width. Here's a very basic clone of the (macOS / Linux) `ls` command which displays a directory listing in columns:
```python
import os
import sys
from rich import print
from rich.columns import Columns
directory = os.listdir(sys.argv[1])
print(Columns(directory))
```
The following screenshot is the output from the [columns example](https://github.com/willmcgugan/rich/blob/master/examples/columns.py) which displays data pulled from an API in columns:
![columns](https://github.com/willmcgugan/rich/raw/master/imgs/columns.png)
</details>
<details>
<summary>Markdown</summary>
Rich can render [markdown](https://rich.readthedocs.io/en/latest/markdown.html) and does a reasonable job of translating the formatting to the terminal.
To render markdown import the `Markdown` class and construct it with a string containing markdown code. Then print it to the console. Here's an example:
```python
from rich.console import Console
from rich.markdown import Markdown
console = Console()
with open("README.md") as readme:
markdown = Markdown(readme.read())
console.print(markdown)
```
This will produce output something like the following:
![markdown](https://github.com/willmcgugan/rich/raw/master/imgs/markdown.png)
</details>
<details>
<summary>Syntax Highlighting</summary>
Rich uses the [pygments](https://pygments.org/) library to implement [syntax highlighting](https://rich.readthedocs.io/en/latest/syntax.html). Usage is similar to rendering markdown; construct a `Syntax` object and print it to the console. Here's an example:
```python
from rich.console import Console
from rich.syntax import Syntax
my_code = '''
def iter_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]:
"""Iterate and generate a tuple with a flag for first and last value."""
iter_values = iter(values)
try:
previous_value = next(iter_values)
except StopIteration:
return
first = True
for value in iter_values:
yield first, False, previous_value
first = False
previous_value = value
yield first, True, previous_value
'''
syntax = Syntax(my_code, "python", theme="monokai", line_numbers=True)
console = Console()
console.print(syntax)
```
This will produce the following output:
![syntax](https://github.com/willmcgugan/rich/raw/master/imgs/syntax.png)
</details>
<details>
<summary>Tracebacks</summary>
Rich can render [beautiful tracebacks](https://rich.readthedocs.io/en/latest/traceback.html) which are easier to read and show more code than standard Python tracebacks. You can set Rich as the default traceback handler so all uncaught exceptions will be rendered by Rich.
Here's what it looks like on OSX (similar on Linux):
![traceback](https://github.com/willmcgugan/rich/raw/master/imgs/traceback.png)
</details>
All Rich renderables make use of the [Console Protocol](https://rich.readthedocs.io/en/latest/protocol.html), which you can also use to implement your own Rich content.
# Rich for enterprise
Available as part of the Tidelift Subscription.
The maintainers of Rich and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. [Learn more.](https://tidelift.com/subscription/pkg/pypi-rich?utm_source=pypi-rich&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
# Project using Rich
Here are a few projects using Rich:
- [BrancoLab/BrainRender](https://github.com/BrancoLab/BrainRender)
a python package for the visualization of three dimensional neuro-anatomical data
- [Ciphey/Ciphey](https://github.com/Ciphey/Ciphey)
Automated decryption tool
- [emeryberger/scalene](https://github.com/emeryberger/scalene)
a high-performance, high-precision CPU and memory profiler for Python
- [hedythedev/StarCli](https://github.com/hedythedev/starcli)
Browse GitHub trending projects from your command line
- [intel/cve-bin-tool](https://github.com/intel/cve-bin-tool)
This tool scans for a number of common, vulnerable components (openssl, libpng, libxml2, expat and a few others) to let you know if your system includes common libraries with known vulnerabilities.
- [nf-core/tools](https://github.com/nf-core/tools)
Python package with helper tools for the nf-core community.
- [cansarigol/pdbr](https://github.com/cansarigol/pdbr)
pdb + Rich library for enhanced debugging
- [plant99/felicette](https://github.com/plant99/felicette)
Satellite imagery for dummies.
- [seleniumbase/SeleniumBase](https://github.com/seleniumbase/SeleniumBase)
Automate & test 10x faster with Selenium & pytest. Batteries included.
- [smacke/ffsubsync](https://github.com/smacke/ffsubsync)
Automagically synchronize subtitles with video.
- [tryolabs/norfair](https://github.com/tryolabs/norfair)
Lightweight Python library for adding real-time 2D object tracking to any detector.
- [ansible/ansible-lint](https://github.com/ansible/ansible-lint) Ansible-lint checks playbooks for practices and behaviour that could potentially be improved
- [ansible-community/molecule](https://github.com/ansible-community/molecule) Ansible Molecule testing framework
- +[Many more](https://github.com/willmcgugan/rich/network/dependents)!
<!-- This is a test, no need to translate -->

View File

@@ -36,12 +36,14 @@ from .css.parse import parse_selectors
from .css.query import NoMatches, QueryType
from .dom import DOMNode
from .geometry import Offset, Region, Size
from .notifications import Notification, SeverityLevel
from .reactive import Reactive
from .renderables.background_screen import BackgroundScreen
from .renderables.blank import Blank
from .timer import Timer
from .widget import Widget
from .widgets import Tooltip
from .widgets._toast import ToastRack
if TYPE_CHECKING:
from typing_extensions import Final
@@ -202,10 +204,12 @@ class Screen(Generic[ScreenResultType], Widget):
Returns:
Tuple of layer names.
"""
if self.app._disable_tooltips:
return super().layers
else:
return (*super().layers, "_tooltips")
extras = []
if not self.app._disable_notifications:
extras.append("_toastrack")
if not self.app._disable_tooltips:
extras.append("_tooltips")
return (*super().layers, *extras)
def render(self) -> RenderableType:
background = self.styles.background
@@ -529,9 +533,18 @@ class Screen(Generic[ScreenResultType], Widget):
self._update_focus_styles(focused, blurred)
def _extend_compose(self, widgets: list[Widget]) -> None:
"""Insert the tooltip widget, if required."""
"""Insert Textual's own internal widgets.
Args:
widgets: The list of widgets to be composed.
This method adds the tooltip, if required, and also adds the
container for `Toast`s.
"""
if not self.app._disable_tooltips:
widgets.insert(0, Tooltip(id="textual-tooltip"))
if not self.app._disable_notifications:
widgets.insert(0, ToastRack(id="textual-toastrack"))
async def _on_idle(self, event: events.Idle) -> None:
# Check for any widgets marked as 'dirty' (needs a repaint)
@@ -727,6 +740,7 @@ class Screen(Generic[ScreenResultType], Widget):
def _on_screen_resume(self) -> None:
"""Screen has resumed."""
self.stack_updates += 1
self.app._refresh_notifications()
size = self.app.size
self._refresh_layout(size, full=True)
self.refresh()
@@ -926,6 +940,30 @@ class Screen(Generic[ScreenResultType], Widget):
"""
self.dismiss(result)
def notify(
self,
message: str,
*,
title: str | None = None,
severity: SeverityLevel = "information",
timeout: float = Notification.timeout,
) -> Notification:
"""Create a notification.
Args:
message: The message for the notification.
title: The title for the notification.
severity: The severity of the notification.
timeout: The timeout for the notification.
Returns:
The new notification.
See [`App.notify`][textual.app.App.notify] for the full
documentation for this method.
"""
return self.app.notify(message, title=title, severity=severity, timeout=timeout)
@rich.repr.auto
class ModalScreen(Screen[ScreenResultType]):

View File

@@ -117,6 +117,7 @@ class ScrollView(ScrollableContainer):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll to a given (absolute) coordinate, optionally animating.
@@ -128,6 +129,7 @@ class ScrollView(ScrollableContainer):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
self._scroll_to(
@@ -138,4 +140,5 @@ class ScrollView(ScrollableContainer):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)

View File

@@ -45,6 +45,7 @@ from ._animator import DEFAULT_EASING, Animatable, BoundAnimator, EasingFunction
from ._arrange import DockArrangeResult, arrange
from ._asyncio import create_task
from ._cache import FIFOCache
from ._callback import invoke
from ._compose import compose
from ._context import NoActiveAppError, active_app
from ._easing import DEFAULT_SCROLL_EASING
@@ -61,6 +62,7 @@ from .geometry import NULL_REGION, NULL_SPACING, Offset, Region, Size, Spacing,
from .layouts.vertical import VerticalLayout
from .message import Message
from .messages import CallbackType
from .notifications import Notification, SeverityLevel
from .reactive import Reactive
from .render import measure
from .strip import Strip
@@ -1696,6 +1698,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> bool:
"""Scroll to a given (absolute) coordinate, optionally animating.
@@ -1707,6 +1710,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and speed is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
Returns:
`True` if the scroll position changed, otherwise `False`.
@@ -1733,6 +1737,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
on_complete=on_complete,
)
scrolled_x = True
if maybe_scroll_y:
@@ -1745,6 +1750,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
on_complete=on_complete,
)
scrolled_y = True
@@ -1760,6 +1766,9 @@ class Widget(DOMNode):
self.scroll_target_y = self.scroll_y = y
scrolled_y = scroll_y != self.scroll_y
if on_complete is not None:
self.call_after_refresh(on_complete)
return scrolled_x or scrolled_y
def scroll_to(
@@ -1772,6 +1781,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll to a given (absolute) coordinate, optionally animating.
@@ -1783,6 +1793,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
Note:
The call to scroll is made after the next refresh.
@@ -1796,6 +1807,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_relative(
@@ -1808,6 +1820,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll relative to current position.
@@ -1819,6 +1832,7 @@ class Widget(DOMNode):
duration: Duration of animation, if animate is `True` and speed is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
self.scroll_to(
None if x is None else (self.scroll_x + x),
@@ -1828,6 +1842,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_home(
@@ -1838,6 +1853,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll to home position.
@@ -1847,6 +1863,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
if speed is None and duration is None:
duration = 1.0
@@ -1858,6 +1875,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_end(
@@ -1868,6 +1886,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll to the end of the container.
@@ -1877,6 +1896,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
if speed is None and duration is None:
duration = 1.0
@@ -1897,6 +1917,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
self.call_after_refresh(_lazily_scroll_end)
@@ -1909,6 +1930,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll one cell left.
@@ -1918,6 +1940,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
self.scroll_to(
x=self.scroll_target_x - 1,
@@ -1926,6 +1949,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def _scroll_left_for_pointer(
@@ -1936,6 +1960,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> bool:
"""Scroll left one position, taking scroll sensitivity into account.
@@ -1945,6 +1970,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
Returns:
`True` if any scrolling was done.
@@ -1960,6 +1986,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_right(
@@ -1970,6 +1997,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll one cell right.
@@ -1979,6 +2007,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
self.scroll_to(
x=self.scroll_target_x + 1,
@@ -1987,6 +2016,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def _scroll_right_for_pointer(
@@ -1997,6 +2027,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> bool:
"""Scroll right one position, taking scroll sensitivity into account.
@@ -2006,6 +2037,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
Returns:
`True` if any scrolling was done.
@@ -2021,6 +2053,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_down(
@@ -2031,6 +2064,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll one line down.
@@ -2040,6 +2074,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
self.scroll_to(
y=self.scroll_target_y + 1,
@@ -2048,6 +2083,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def _scroll_down_for_pointer(
@@ -2058,6 +2094,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> bool:
"""Scroll down one position, taking scroll sensitivity into account.
@@ -2067,6 +2104,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
Returns:
`True` if any scrolling was done.
@@ -2082,6 +2120,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_up(
@@ -2092,6 +2131,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll one line up.
@@ -2101,6 +2141,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and speed is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
self.scroll_to(
y=self.scroll_target_y - 1,
@@ -2109,6 +2150,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def _scroll_up_for_pointer(
@@ -2119,6 +2161,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> bool:
"""Scroll up one position, taking scroll sensitivity into account.
@@ -2128,6 +2171,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and speed is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
Returns:
`True` if any scrolling was done.
@@ -2143,6 +2187,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_page_up(
@@ -2153,6 +2198,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll one page up.
@@ -2162,6 +2208,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
self.scroll_to(
y=self.scroll_y - self.container_size.height,
@@ -2170,6 +2217,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_page_down(
@@ -2180,6 +2228,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll one page down.
@@ -2189,6 +2238,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
self.scroll_to(
y=self.scroll_y + self.container_size.height,
@@ -2197,6 +2247,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_page_left(
@@ -2207,6 +2258,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll one page left.
@@ -2216,6 +2268,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
if speed is None and duration is None:
duration = 0.3
@@ -2226,6 +2279,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_page_right(
@@ -2236,6 +2290,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll one page right.
@@ -2245,6 +2300,7 @@ class Widget(DOMNode):
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
if speed is None and duration is None:
duration = 0.3
@@ -2255,6 +2311,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_to_widget(
@@ -2269,6 +2326,7 @@ class Widget(DOMNode):
top: bool = False,
origin_visible: bool = True,
force: bool = False,
on_complete: CallbackType | None = None,
) -> bool:
"""Scroll scrolling to bring a widget in to view.
@@ -2281,6 +2339,7 @@ class Widget(DOMNode):
top: Scroll widget to top of container.
origin_visible: Ensure that the top left of the widget is within the window.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
Returns:
`True` if any scrolling has occurred in any descendant, otherwise `False`.
@@ -2306,6 +2365,7 @@ class Widget(DOMNode):
easing=easing,
origin_visible=origin_visible,
force=force,
on_complete=on_complete,
)
if scroll_offset:
scrolled = True
@@ -2339,6 +2399,7 @@ class Widget(DOMNode):
top: bool = False,
origin_visible: bool = True,
force: bool = False,
on_complete: CallbackType | None = None,
) -> Offset:
"""Scrolls a given region in to view, if required.
@@ -2355,6 +2416,7 @@ class Widget(DOMNode):
top: Scroll `region` to top of container.
origin_visible: Ensure that the top left of the widget is within the window.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
Returns:
The distance that was scrolled.
@@ -2400,6 +2462,7 @@ class Widget(DOMNode):
duration=duration,
easing=easing,
force=force,
on_complete=on_complete,
)
return delta
@@ -2412,6 +2475,7 @@ class Widget(DOMNode):
top: bool = False,
easing: EasingFunction | str | None = None,
force: bool = False,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll the container to make this widget visible.
@@ -2422,6 +2486,7 @@ class Widget(DOMNode):
top: Scroll to top of container.
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
"""
parent = self.parent
if isinstance(parent, Widget):
@@ -2434,6 +2499,7 @@ class Widget(DOMNode):
top=top,
easing=easing,
force=force,
on_complete=on_complete,
)
def scroll_to_center(
@@ -2446,6 +2512,7 @@ class Widget(DOMNode):
easing: EasingFunction | str | None = None,
force: bool = False,
origin_visible: bool = True,
on_complete: CallbackType | None = None,
) -> None:
"""Scroll this widget to the center of self.
@@ -2459,6 +2526,7 @@ class Widget(DOMNode):
easing: An easing method for the scrolling animation.
force: Force scrolling even when prohibited by overflow styling.
origin_visible: Ensure that the top left corner of the widget remains visible after the scroll.
on_complete: A callable to invoke when the animation is finished.
"""
self.call_after_refresh(
@@ -2471,6 +2539,7 @@ class Widget(DOMNode):
force=force,
center=True,
origin_visible=origin_visible,
on_complete=on_complete,
)
def can_view(self, widget: Widget) -> bool:
@@ -3240,3 +3309,27 @@ class Widget(DOMNode):
if not self.allow_vertical_scroll:
raise SkipAction()
self.scroll_page_up()
def notify(
self,
message: str,
*,
title: str | None = None,
severity: SeverityLevel = "information",
timeout: float = Notification.timeout,
) -> Notification:
"""Create a notification.
Args:
message: The message for the notification.
title: The title for the notification.
severity: The severity of the notification.
timeout: The timeout for the notification.
Returns:
The new notification.
See [`App.notify`][textual.app.App.notify] for the full
documentation for this method.
"""
return self.app.notify(message, title=title, severity=severity, timeout=timeout)

View File

@@ -408,7 +408,6 @@ class OptionList(ScrollView, can_focus=True):
Args:
event: The mouse movement event.
"""
print(event, event.style.meta)
self._mouse_hovering_over = event.style.meta.get("option")
def _on_leave(self, _: Leave) -> None:

View File

@@ -536,6 +536,11 @@ class SelectionList(Generic[SelectionType], OptionList):
# BUTTON_LEFT and BUTTON_RIGHT.
side_style = Style.from_color(button_style.bgcolor, underlying_style.bgcolor)
# Add the option index to the style. This is used to determine which
# option to select when the button is clicked or hovered.
side_style += Style(meta={"option": selection_index})
button_style += Style(meta={"option": selection_index})
# At this point we should have everything we need to place a
# "button" before the option.
return Strip(

View File

@@ -0,0 +1,191 @@
"""Widgets for showing notification messages in toasts."""
from __future__ import annotations
from rich.console import RenderableType
from rich.text import Text
from .. import on
from ..containers import Container
from ..css.query import NoMatches
from ..events import Click, Mount
from ..notifications import Notification, Notifications
from ._static import Static
class ToastHolder(Container, inherit_css=False):
"""Container that holds a single toast.
Used to control the alignment of each of the toasts in the main toast
container.
"""
DEFAULT_CSS = """
ToastHolder {
align-horizontal: right;
width: 1fr;
height: auto;
visibility: hidden;
}
"""
class Toast(Static, inherit_css=False):
"""A widget for displaying short-lived notifications."""
DEFAULT_CSS = """
Toast {
width: 60;
max-width: 50%;
height: auto;
visibility: visible;
margin-top: 1;
padding: 1 1;
background: $panel;
tint: white 5%;
}
.toast--title {
text-style: bold;
}
Toast {
border-right: wide $background;
}
Toast.-information {
border-left: wide $success;
}
Toast.-information .toast--title {
color: $success-darken-1;
}
Toast.-warning {
border-left: wide $warning;
}
Toast.-warning .toast--title {
color: $warning-darken-1;
}
Toast.-error {
border-left: wide $error;
}
Toast.-error .toast--title {
color: $error-darken-1;
}
Toast.-empty-title {
}
"""
COMPONENT_CLASSES = {"toast--title"}
def __init__(self, notification: Notification) -> None:
"""Initialise the toast.
Args:
notification: The notification to show in the toast.
"""
super().__init__(
classes=f"-{notification.severity} {'-empty-title' if not notification.title else ''}"
)
self._notification = notification
self._timeout = notification.time_left
def render(self) -> RenderableType:
notification = self._notification
if notification.title:
header_style = self.get_component_rich_style("toast--title")
notification_text = Text.assemble(
(notification.title, header_style),
"\n",
Text.from_markup(notification.message),
)
else:
notification_text = Text.assemble(
Text.from_markup(notification.message),
)
return notification_text
def _on_mount(self, _: Mount) -> None:
"""Set the time running once the toast is mounted."""
self.set_timer(self._timeout, self._expire)
@on(Click)
def _expire(self) -> None:
"""Remove the toast once the timer has expired."""
# Before we removed ourself, we also call on the app to forget about
# the notification that caused us to exist. Note that we tell the
# app to not bother refreshing the display on our account, we're
# about to handle that anyway.
self.app.unnotify(self._notification, refresh=False)
# Note that we attempt to remove our parent, because we're wrapped
# inside an alignment container. The testing that we are is as much
# to keep type checkers happy as anything else.
(self.parent if isinstance(self.parent, ToastHolder) else self).remove()
class ToastRack(Container, inherit_css=False):
"""A container for holding toasts."""
DEFAULT_CSS = """
ToastRack {
layer: _toastrack;
width: 1fr;
height: auto;
dock: top;
align: right bottom;
visibility: hidden;
layout: vertical;
overflow-y: scroll;
margin-bottom: 1;
margin-right: 1;
}
"""
@staticmethod
def _toast_id(notification: Notification) -> str:
"""Create a Textual-DOM-internal ID for the given notification.
Args:
notification: The notification to create the ID for.
Returns:
An ID for the notification that can be used within the DOM.
"""
return f"--textual-toast-{notification.identity}"
def show(self, notifications: Notifications) -> None:
"""Show the notifications as toasts.
Args:
notifications: The notifications to show.
"""
# Look for any stale toasts and remove them.
for toast in self.query(Toast):
if toast._notification not in notifications:
toast.remove()
# Gather up all the notifications that we don't have toasts for yet.
new_toasts: list[Notification] = []
for notification in notifications:
try:
# See if there's already a toast for that notification.
_ = self.get_child_by_id(self._toast_id(notification))
except NoMatches:
if not notification.has_expired:
new_toasts.append(notification)
# If we got any...
if new_toasts:
# ...mount them.
self.mount_all(
ToastHolder(Toast(toast), id=self._toast_id(toast))
for toast in new_toasts
)
self.call_later(self.scroll_end, animate=False, force=True)

View File

@@ -0,0 +1,28 @@
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widget import Widget
class NotifyWidget(Widget):
def on_mount(self) -> None:
self.notify("test", timeout=60)
class NotifyScreen(Screen):
def on_mount(self) -> None:
self.notify("test", timeout=60)
def compose(self) -> ComposeResult:
yield NotifyWidget()
class NotifyApp(App[None]):
def on_mount(self) -> None:
self.notify("test", timeout=60)
self.push_screen(NotifyScreen())
async def test_all_levels_of_notification() -> None:
"""All levels within the DOM should be able to notify."""
async with NotifyApp().run_test() as pilot:
assert len(pilot.app._notifications) == 3

View File

@@ -0,0 +1,49 @@
from time import sleep
from textual.app import App
class NotificationApp(App[None]):
pass
async def test_app_no_notifications() -> None:
"""An app with no notifications should have an empty notification list."""
async with NotificationApp().run_test() as pilot:
assert len(pilot.app._notifications) == 0
async def test_app_with_notifications() -> None:
"""An app with notifications should have notifications in the list."""
async with NotificationApp().run_test() as pilot:
pilot.app.notify("test")
assert len(pilot.app._notifications) == 1
async def test_app_with_removing_notifications() -> None:
"""An app with notifications should have notifications in the list, which can be removed."""
async with NotificationApp().run_test() as pilot:
notification = pilot.app.notify("test")
assert len(pilot.app._notifications) == 1
pilot.app.unnotify(notification)
assert len(pilot.app._notifications) == 0
async def test_app_with_notifications_that_expire() -> None:
"""Notifications should expire from an app."""
async with NotificationApp().run_test() as pilot:
for n in range(100):
pilot.app.notify("test", timeout=(0.5 if bool(n % 2) else 60))
assert len(pilot.app._notifications) == 100
sleep(0.6)
assert len(pilot.app._notifications) == 50
async def test_app_clearing_notifications() -> None:
"""The application should be able to clear all notifications."""
async with NotificationApp().run_test() as pilot:
for _ in range(100):
pilot.app.notify("test", timeout=120)
assert len(pilot.app._notifications) == 100
pilot.app.clear_notifications()
assert len(pilot.app._notifications) == 0

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
from time import sleep
from textual.notifications import Notification
def test_message() -> None:
"""A notification should not change the message."""
assert Notification("test").message == "test"
def test_default_title() -> None:
"""A notification with no title should have a None title."""
assert Notification("test").title is None
def test_default_severity_level() -> None:
"""The default severity level should be as expected."""
assert Notification("test").severity == "information"
def test_default_timeout() -> None:
"""The default timeout should be as expected."""
assert Notification("test").timeout == 3
def test_identity_is_unique() -> None:
"""A collection of notifications should, by default, have unique IDs."""
notifications: set[str] = set()
for _ in range(1000):
notifications.add(Notification("test").identity)
assert len(notifications) == 1000
def test_time_out() -> None:
test = Notification("test", timeout=0.5)
assert test.has_expired is False
sleep(0.6)
assert test.has_expired is True

View File

@@ -0,0 +1,78 @@
from __future__ import annotations
from time import sleep
from textual.notifications import Notification, Notifications
def test_empty_to_start_with() -> None:
"""We should have no notifications if we've not raised any."""
assert len(Notifications()) == 0
def test_many_notifications() -> None:
"""Adding lots of long-timeout notifications should result in them being in the list."""
tester = Notifications()
for _ in range(100):
tester.add(Notification("test", timeout=60))
assert len(tester) == 100
def test_timeout() -> None:
"""Notifications should timeout from the list."""
tester = Notifications()
for n in range(100):
tester.add(Notification("test", timeout=(0.5 if bool(n % 2) else 60)))
assert len(tester) == 100
sleep(0.6)
assert len(tester) == 50
def test_in() -> None:
"""It should be possible to test if a notification is in a collection."""
tester = Notifications()
within = Notification("within", timeout=120)
outwith = Notification("outwith", timeout=120)
tester.add(within)
assert within in tester
assert outwith not in tester
def test_remove_notification() -> None:
"""It should be possible to remove a notification."""
tester = Notifications()
first = Notification("first", timeout=120)
second = Notification("second", timeout=120)
third = Notification("third", timeout=120)
tester.add(first)
tester.add(second)
tester.add(third)
assert list(tester) == [first, second, third]
del tester[second]
assert list(tester) == [first, third]
del tester[first]
assert list(tester) == [third]
del tester[third]
assert list(tester) == []
def test_remove_notification_multiple_times() -> None:
"""It should be possible to remove the same notification more than once without an error."""
tester = Notifications()
alert = Notification("delete me")
tester.add(alert)
assert list(tester) == [alert]
del tester[alert]
assert list(tester) == []
del tester[alert]
assert list(tester) == []
def test_clear() -> None:
"""It should be possible to clear all notifications."""
tester = Notifications()
for _ in range(100):
tester.add(Notification("test", timeout=120))
assert len(tester) == 100
tester.clear()
assert len(tester) == 0

View File

@@ -0,0 +1,42 @@
"""See https://github.com/Textualize/textual/pull/2930 for the introduction of these tests."""
from textual import on
from textual.app import App, ComposeResult
from textual.geometry import Offset
from textual.widgets import SelectionList
class SelectionListApp(App[None]):
"""Test selection list application."""
def __init__(self) -> None:
super().__init__()
self.clicks: list[int] = []
def compose(self) -> ComposeResult:
yield SelectionList[int](*[(str(n), n) for n in range(10)])
@on(SelectionList.SelectionToggled)
def _record(self, event: SelectionList.SelectionToggled) -> None:
assert event.control == self.query_one(SelectionList)
self.clicks.append(event.selection_index)
async def test_click_on_prompt() -> None:
"""It should be possible to toggle a selection by clicking on the prompt."""
async with SelectionListApp().run_test() as pilot:
assert isinstance(pilot.app, SelectionListApp)
await pilot.click(SelectionList, Offset(5,1))
await pilot.pause()
assert pilot.app.clicks == [0]
async def test_click_on_checkbox() -> None:
"""It should be possible to toggle a selection by clicking on the checkbox."""
async with SelectionListApp().run_test() as pilot:
assert isinstance(pilot.app, SelectionListApp)
await pilot.click(SelectionList, Offset(3,1))
await pilot.pause()
assert pilot.app.clicks == [0]
if __name__ == "__main__":
SelectionListApp().run()

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,26 @@
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Label
class Mode(Screen):
def compose(self) -> ComposeResult:
yield Label("This is a mode screen")
class NotifyThroughModesApp(App[None]):
MODES = {
"test": Mode()
}
def compose(self) -> ComposeResult:
yield Label("Base screen")
def on_mount(self):
for n in range(10):
self.notify(str(n))
self.switch_mode("test")
if __name__ == "__main__":
NotifyThroughModesApp().run()

View File

@@ -0,0 +1,31 @@
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Label
class StackableScreen(Screen):
TARGET_DEPTH = 10
def __init__(self, count:int = TARGET_DEPTH) -> None:
super().__init__()
self._number = count
def compose(self) -> ComposeResult:
yield Label(f"Screen {self.TARGET_DEPTH - self._number}")
def on_mount(self) -> None:
if self._number > 0:
self.app.push_screen(StackableScreen(self._number - 1))
class NotifyDownScreensApp(App[None]):
def compose(self) -> ComposeResult:
yield Label("Base screen")
def on_mount(self):
for n in range(10):
self.notify(str(n))
self.push_screen(StackableScreen())
if __name__ == "__main__":
NotifyDownScreensApp().run()

View File

@@ -575,3 +575,13 @@ def test_textual_dev_easing_preview(snap_compare):
def test_textual_dev_keys_preview(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "dev_previews_keys.py", press=["a", "b"])
def test_notifications_example(snap_compare) -> None:
assert snap_compare(WIDGET_EXAMPLES_DIR / "toast.py")
def test_notifications_through_screens(snap_compare) -> None:
assert snap_compare(SNAPSHOT_APPS_DIR / "notification_through_screens.py")
def test_notifications_through_modes(snap_compare) -> None:
assert snap_compare(SNAPSHOT_APPS_DIR / "notification_through_modes.py")