mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into unfootgun-worker-thread
This commit is contained in:
@@ -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
|
||||
|
||||
80
docs/blog/posts/release0-30-0.md
Normal file
80
docs/blog/posts/release0-30-0.md
Normal 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).
|
||||
26
docs/examples/widgets/toast.py
Normal file
26
docs/examples/widgets/toast.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
79
docs/widgets/toast.md
Normal 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
283
poetry.lock
generated
@@ -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]
|
||||
|
||||
@@ -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>"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
108
src/textual/notifications.py
Normal file
108
src/textual/notifications.py
Normal 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
|
||||
@@ -1,444 +0,0 @@
|
||||
[](https://pepy.tech/project/rich)
|
||||
[](https://badge.fury.io/py/rich)
|
||||
[](https://codecov.io/gh/willmcgugan/rich)
|
||||
[](https://www.willmcgugan.com/tag/rich/)
|
||||
[](https://twitter.com/willmcgugan)
|
||||
|
||||

|
||||
|
||||
[中文 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.
|
||||
|
||||

|
||||
|
||||
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())
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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()
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
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].")
|
||||
```
|
||||
|
||||

|
||||
|
||||
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)
|
||||
```
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
</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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
</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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
</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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
</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:
|
||||
|
||||

|
||||
|
||||
</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:
|
||||
|
||||

|
||||
|
||||
</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):
|
||||
|
||||

|
||||
|
||||
</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 -->
|
||||
@@ -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]):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
191
src/textual/widgets/_toast.py
Normal file
191
src/textual/widgets/_toast.py
Normal 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)
|
||||
28
tests/notifications/test_all_levels_notifications.py
Normal file
28
tests/notifications/test_all_levels_notifications.py
Normal 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
|
||||
49
tests/notifications/test_app_notifications.py
Normal file
49
tests/notifications/test_app_notifications.py
Normal 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
|
||||
40
tests/notifications/test_notification.py
Normal file
40
tests/notifications/test_notification.py
Normal 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
|
||||
78
tests/notifications/test_notifications.py
Normal file
78
tests/notifications/test_notifications.py
Normal 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
|
||||
42
tests/selection_list/test_selection_click_checkbox.py
Normal file
42
tests/selection_list/test_selection_click_checkbox.py
Normal 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
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user