Merge branch 'main' into fix-texlog-maxlines

This commit is contained in:
Will McGugan
2022-10-31 13:38:55 +00:00
committed by GitHub
50 changed files with 1718 additions and 922 deletions

View File

@@ -8,7 +8,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.7", "3.8", "3.9", "3.10"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
defaults: defaults:
run: run:
shell: bash shell: bash
@@ -20,9 +20,9 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
architecture: x64 architecture: x64
- name: Install and configure Poetry - name: Install and configure Poetry
uses: snok/install-poetry@v1.1.6 uses: snok/install-poetry@v1.3.3
with: with:
version: 1.1.6 version: 1.2.2
virtualenvs-in-project: true virtualenvs-in-project: true
- name: Install dependencies - name: Install dependencies
run: poetry install --extras "dev" run: poetry install --extras "dev"

3
.gitignore vendored
View File

@@ -116,3 +116,6 @@ venv.bak/
# Snapshot testing report output directory # Snapshot testing report output directory
tests/snapshot_tests/output tests/snapshot_tests/output
# Sandbox folder - convenient place for us to develop small test apps without leaving the repo
sandbox/

View File

@@ -7,9 +7,24 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.2.2] - Unreleased ## [0.2.2] - Unreleased
### Fixed
- Fixed issue where scrollbars weren't being unmounted
### Changed ### Changed
- DOMQuery now raises InvalidQueryFormat in response to invalid query strings, rather than cryptic CSS error - DOMQuery now raises InvalidQueryFormat in response to invalid query strings, rather than cryptic CSS error
- Dropped quit_after, screenshot, and screenshot_title from App.run, which can all be done via auto_pilot
- Widgets are now closed in reversed DOM order
### Added
- Added Unmount event
- Added App.run_async method
- Added App.run_test context manager
- Added auto_pilot to App.run and App.run_async
- Added Widget._get_virtual_dom to get scrollbars
- Added size parameter to run and run_async
## [0.2.1] - 2022-10-23 ## [0.2.1] - 2022-10-23

View File

@@ -2,8 +2,9 @@ Stopwatch {
layout: horizontal; layout: horizontal;
background: $boost; background: $boost;
height: 5; height: 5;
padding: 1;
margin: 1; margin: 1;
min-width: 50;
padding: 1;
} }
TimeDisplay { TimeDisplay {

View File

@@ -2,8 +2,8 @@ Stopwatch {
layout: horizontal; layout: horizontal;
background: $boost; background: $boost;
height: 5; height: 5;
min-width: 50;
margin: 1; margin: 1;
min-width: 50;
padding: 1; padding: 1;
} }

View File

@@ -44,7 +44,7 @@ If you combine the `run` command with the `--dev` switch your app will run in *d
textual run --dev my_app.py textual run --dev my_app.py
``` ```
One of the the features of *dev* mode is live editing of CSS files: any changes to your CSS will be reflected in the terminal a few milliseconds later. One of the features of *dev* mode is live editing of CSS files: any changes to your CSS will be reflected in the terminal a few milliseconds later.
This is a great feature for iterating on your app's look and feel. Open the CSS in your editor and have your app running in a terminal. Edits to your CSS will appear almost immediately after you save. This is a great feature for iterating on your app's look and feel. Open the CSS in your editor and have your app running in a terminal. Edits to your CSS will appear almost immediately after you save.

View File

@@ -4,7 +4,7 @@ We've used event handler methods in many of the examples in this guide. This cha
## Messages ## Messages
Events are a particular kind of *message* sent by Textual in response to input and other state changes. Events are reserved for use by Textual but you can also create custom messages for the purpose of coordinating between widgets in your app. Events are a particular kind of *message* sent by Textual in response to input and other state changes. Events are reserved for use by Textual, but you can also create custom messages for the purpose of coordinating between widgets in your app.
More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is true of events. More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is true of events.
@@ -12,7 +12,7 @@ More on that later, but for now keep in mind that events are also messages, and
Every [App][textual.app.App] and [Widget][textual.widget.Widget] object contains a *message queue*. You can think of a message queue as orders at a restaurant. The chef takes an order and makes the dish. Orders that arrive while the chef is cooking are placed in a line. When the chef has finished a dish they pick up the next order in the line. Every [App][textual.app.App] and [Widget][textual.widget.Widget] object contains a *message queue*. You can think of a message queue as orders at a restaurant. The chef takes an order and makes the dish. Orders that arrive while the chef is cooking are placed in a line. When the chef has finished a dish they pick up the next order in the line.
Textual processes messages in the same way. Messages are picked off a queue and processed (cooked) by a handler method. This guarantees messages and events are processed even if your code can not handle them right way. Textual processes messages in the same way. Messages are picked off a queue and processed (cooked) by a handler method. This guarantees messages and events are processed even if your code can not handle them right away.
This processing of messages is done within an asyncio Task which is started when you mount the widget. The task monitors a queue for new messages and dispatches them to the appropriate handler when they arrive. This processing of messages is done within an asyncio Task which is started when you mount the widget. The task monitors a queue for new messages and dispatches them to the appropriate handler when they arrive.
@@ -28,7 +28,7 @@ The widget's task will pick the first message from the queue (a key event for th
--8<-- "docs/images/events/queue.excalidraw.svg" --8<-- "docs/images/events/queue.excalidraw.svg"
</div> </div>
When the `on_key` method returns, Textual will get the next event from the the queue and repeat the process for the remaining keys. At some point the queue will be empty and the widget is said to be in an *idle* state. When the `on_key` method returns, Textual will get the next event from the queue and repeat the process for the remaining keys. At some point the queue will be empty and the widget is said to be in an *idle* state.
!!! note !!! note
@@ -75,7 +75,7 @@ As before, the event bubbles to its parent (the App class).
--8<-- "docs/images/events/bubble3.excalidraw.svg" --8<-- "docs/images/events/bubble3.excalidraw.svg"
</div> </div>
The App class is always the root of the DOM, so there is no where for the event to bubble to. The App class is always the root of the DOM, so there is nowhere for the event to bubble to.
### Stopping bubbling ### Stopping bubbling
@@ -110,7 +110,7 @@ The message class is defined within the widget class itself. This is not strictl
## Sending events ## Sending events
In the previous example we used [emit()][textual.message_pump.MessagePump.emit] to send an event to it's parent. We could also have used [emit_no_wait()][textual.message_pump.MessagePump.emit_no_wait] for non async code. Sending messages in this way allows you to write custom widgets without needing to know in what context they will be used. In the previous example we used [emit()][textual.message_pump.MessagePump.emit] to send an event to its parent. We could also have used [emit_no_wait()][textual.message_pump.MessagePump.emit_no_wait] for non async code. Sending messages in this way allows you to write custom widgets without needing to know in what context they will be used.
There are other ways of sending (posting) messages, which you may need to use less frequently. There are other ways of sending (posting) messages, which you may need to use less frequently.
@@ -127,7 +127,7 @@ Most of the logic in a Textual app will be written in message handlers. Let's ex
Textual uses the following scheme to map messages classes on to a Python method. Textual uses the following scheme to map messages classes on to a Python method.
- Start with `"on_"`. - Start with `"on_"`.
- Add the messages namespace (if any) converted from CamelCase to snake_case plus an underscore `"_"` - Add the messages' namespace (if any) converted from CamelCase to snake_case plus an underscore `"_"`
- Add the name of the class converted from CamelCase to snake_case. - Add the name of the class converted from CamelCase to snake_case.
<div class="excalidraw"> <div class="excalidraw">
@@ -156,7 +156,7 @@ This pattern is a convenience that saves writing out a parameter that may not be
Message handlers may be coroutines. If you prefix your handlers with the `async` keyword, Textual will `await` them. This lets your handler use the `await` keyword for asynchronous APIs. Message handlers may be coroutines. If you prefix your handlers with the `async` keyword, Textual will `await` them. This lets your handler use the `await` keyword for asynchronous APIs.
If your event handlers are coroutines it will allow multiple events to be processed concurrently, but bear in mind an individual widget (or app) will not be able to pick up a new message from its message queue until the handler has returned. This is rarely a problem in practice; as long has handlers return within a few milliseconds the UI will remain responsive. But slow handlers might make your app hard to use. If your event handlers are coroutines it will allow multiple events to be processed concurrently, but bear in mind an individual widget (or app) will not be able to pick up a new message from its message queue until the handler has returned. This is rarely a problem in practice; as long as handlers return within a few milliseconds the UI will remain responsive. But slow handlers might make your app hard to use.
!!! info !!! info

View File

@@ -165,7 +165,11 @@ If you click the buttons in the above example it will show the current count. Wh
## Watch methods ## Watch methods
Watch methods are another superpower. Textual will call watch methods when reactive attributes are modified. Watch methods begin with `watch_` followed by the name of the attribute. If the watch method accepts a positional argument, it will be called with the new assigned value. If the watch method accepts *two* positional arguments, it will be called with both the *old* value and the *new* value. Watch methods are another superpower.
Textual will call watch methods when reactive attributes are modified.
Watch methods begin with `watch_` followed by the name of the attribute.
If the watch method accepts a positional argument, it will be called with the new assigned value.
If the watch method accepts *two* positional arguments, it will be called with both the *old* value and the *new* value.
The following app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example `"darkorchid"` or `"#52de44"`. The following app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example `"darkorchid"` or `"#52de44"`.
@@ -192,6 +196,12 @@ The following app will display any color you type in to the input. Try it with a
The color is parsed in `on_input_submitted` and assigned to `self.color`. Because `color` is reactive, Textual also calls `watch_color` with the old and new values. The color is parsed in `on_input_submitted` and assigned to `self.color`. Because `color` is reactive, Textual also calls `watch_color` with the old and new values.
### When are watch methods called?
Textual only calls watch methods if the value of a reactive attribute _changes_.
If the newly assigned value is the same as the previous value, the watch method is not called.
You can override this behaviour by passing `always_update=True` to `reactive`.
## Compute methods ## Compute methods
Compute methods are the final superpower offered by the `reactive` descriptor. Textual runs compute methods to calculate the value of a reactive attribute. Compute methods begin with `compute_` followed by the name of the reactive value. Compute methods are the final superpower offered by the `reactive` descriptor. Textual runs compute methods to calculate the value of a reactive attribute. Compute methods begin with `compute_` followed by the name of the reactive value.

1
docs/reference/pilot.md Normal file
View File

@@ -0,0 +1 @@
::: textual.pilot

View File

@@ -108,7 +108,7 @@ Let's examine `stopwatch01.py` in more detail.
--8<-- "docs/examples/tutorial/stopwatch01.py" --8<-- "docs/examples/tutorial/stopwatch01.py"
``` ```
The first line imports the Textual `App` class, which we will use as the base class for our App. The second line imports two builtin widgets: `Footer` which shows a bar at the bottom of the screen with current keys, and `Header` which shows a title and the current time at the top of the screen. Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build widgets in this tutorial. The first line imports the Textual `App` class, which we will use as the base class for our App. The second line imports two builtin widgets: `Footer` which shows a bar at the bottom of the screen with bound keys, and `Header` which shows a title at the top of the screen. Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build widgets in this tutorial.
The following lines define the app itself: The following lines define the app itself:
@@ -165,7 +165,7 @@ The Stopwatch widget class also extends `Static`. This class has a `compose()` m
#### The buttons #### The buttons
The Button constructor takes a label to be displayed in the button (`"Start"`, `"Stop"`, or `"Reset"`). Additionally some of the buttons set the following parameters: The Button constructor takes a label to be displayed in the button (`"Start"`, `"Stop"`, or `"Reset"`). Additionally, some of the buttons set the following parameters:
- `id` is an identifier we can use to tell the buttons apart in code and apply styles. More on that later. - `id` is an identifier we can use to tell the buttons apart in code and apply styles. More on that later.
- `variant` is a string which selects a default style. The "success" variant makes the button green, and the "error" variant makes it red. - `variant` is a string which selects a default style. The "success" variant makes the button green, and the "error" variant makes it red.
@@ -233,8 +233,9 @@ Stopwatch {
layout: horizontal; layout: horizontal;
background: $boost; background: $boost;
height: 5; height: 5;
padding: 1;
margin: 1; margin: 1;
min-width: 50;
padding: 1;
} }
``` ```
@@ -249,8 +250,9 @@ Here's how this CSS code changes how the `Stopwatch` widget is displayed.
- `layout: horizontal` aligns child widgets horizontally from left to right. - `layout: horizontal` aligns child widgets horizontally from left to right.
- `background: $boost` sets the background color to `$boost`. The `$` prefix picks a pre-defined color from the builtin theme. There are other ways to specify colors such as `"blue"` or `rgb(20,46,210)`. - `background: $boost` sets the background color to `$boost`. The `$` prefix picks a pre-defined color from the builtin theme. There are other ways to specify colors such as `"blue"` or `rgb(20,46,210)`.
- `height: 5` sets the height of our widget to 5 lines of text. - `height: 5` sets the height of our widget to 5 lines of text.
- `padding: 1` sets a padding of 1 cell around the child widgets.
- `margin: 1` sets a margin of 1 cell around the `Stopwatch` widget to create a little space between widgets in the list. - `margin: 1` sets a margin of 1 cell around the `Stopwatch` widget to create a little space between widgets in the list.
- `min-width: 50` sets the minimum width of our widget to 50 cells.
- `padding: 1` sets a padding of 1 cell around the child widgets.
Here's the rest of `stopwatch03.css` which contains further declaration blocks: Here's the rest of `stopwatch03.css` which contains further declaration blocks:
@@ -288,7 +290,7 @@ The last 3 blocks have a slightly different format. When the declaration begins
The buttons have a `dock` style which aligns the widget to a given edge. The start and stop buttons are docked to the left edge, while the reset button is docked to the right edge. The buttons have a `dock` style which aligns the widget to a given edge. The start and stop buttons are docked to the left edge, while the reset button is docked to the right edge.
You may have noticed that the stop button (`#stop` in the CSS) has `display: none;`. This tells Textual to not show the button. We do this because we don't want to display the stop button when the timer is *not* running. Similarly we don't want to show the start button when the timer is running. We will cover how to manage such dynamic user interfaces in the next section. You may have noticed that the stop button (`#stop` in the CSS) has `display: none;`. This tells Textual to not show the button. We do this because we don't want to display the stop button when the timer is *not* running. Similarly, we don't want to show the start button when the timer is running. We will cover how to manage such dynamic user interfaces in the next section.
### Dynamic CSS ### Dynamic CSS
@@ -333,7 +335,7 @@ The following code will start or stop the stopwatches in response to clicking a
--8<-- "docs/examples/tutorial/stopwatch04.py" --8<-- "docs/examples/tutorial/stopwatch04.py"
``` ```
The `on_button_pressed` method is an *event handler*. Event handlers are methods called by Textual in response to an *event* such as a key press, mouse click, etc. Event handlers begin with `on_` followed by the name of the event they will handler. Hence `on_button_pressed` will handle the button pressed event. The `on_button_pressed` method is an *event handler*. Event handlers are methods called by Textual in response to an *event* such as a key press, mouse click, etc. Event handlers begin with `on_` followed by the name of the event they will handle. Hence `on_button_pressed` will handle the button pressed event.
If you run `stopwatch04.py` now you will be able to toggle between the two states by clicking the first button: If you run `stopwatch04.py` now you will be able to toggle between the two states by clicking the first button:

View File

@@ -107,6 +107,7 @@ nav:
- "reference/index.md" - "reference/index.md"
- "reference/message_pump.md" - "reference/message_pump.md"
- "reference/message.md" - "reference/message.md"
- "reference/pilot.md"
- "reference/query.md" - "reference/query.md"
- "reference/reactive.md" - "reference/reactive.md"
- "reference/screen.md" - "reference/screen.md"

631
poetry.lock generated
View File

@@ -18,7 +18,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
yarl = ">=1.0,<2.0" yarl = ">=1.0,<2.0"
[package.extras] [package.extras]
speedups = ["aiodns", "brotli", "cchardet"] speedups = ["Brotli", "aiodns", "cchardet"]
[[package]] [[package]]
name = "aiosignal" name = "aiosignal"
@@ -59,10 +59,10 @@ optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[package.extras] [package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]] [[package]]
name = "black" name = "black"
@@ -120,11 +120,11 @@ optional = false
python-versions = ">=3.6.0" python-versions = ">=3.6.0"
[package.extras] [package.extras]
unicode_backport = ["unicodedata2"] unicode-backport = ["unicodedata2"]
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.2" version = "8.1.3"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
category = "main" category = "main"
optional = false optional = false
@@ -136,11 +136,11 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.5" version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
[[package]] [[package]]
name = "colored" name = "colored"
@@ -180,6 +180,17 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "exceptiongroup"
version = "1.0.0"
description = "Backport of PEP 654 (exception groups)"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
test = ["pytest (>=6)"]
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.8.0" version = "3.8.0"
@@ -212,11 +223,11 @@ python-versions = "*"
python-dateutil = ">=2.8.1" python-dateutil = ">=2.8.1"
[package.extras] [package.extras]
dev = ["twine", "markdown", "flake8", "wheel"] dev = ["flake8", "markdown", "twine", "wheel"]
[[package]] [[package]]
name = "griffe" name = "griffe"
version = "0.22.2" version = "0.23.0"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
category = "dev" category = "dev"
optional = false optional = false
@@ -230,7 +241,7 @@ async = ["aiofiles (>=0.7,<1.0)"]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.5.6" version = "2.5.8"
description = "File identification library for Python" description = "File identification library for Python"
category = "dev" category = "dev"
optional = false optional = false
@@ -260,9 +271,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5" zipp = ">=0.5"
[package.extras] [package.extras]
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
perf = ["ipython"] perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
@@ -339,8 +350,8 @@ typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""}
watchdog = ">=2.0" watchdog = ">=2.0"
[package.extras] [package.extras]
min-versions = ["watchdog (==2.0)", "typing-extensions (==3.10)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "packaging (==20.5)", "mergedeep (==1.3.4)", "markupsafe (==2.0.1)", "markdown (==3.2.1)", "jinja2 (==2.11.1)", "importlib-metadata (==4.3)", "ghp-import (==1.0)", "colorama (==0.4)", "click (==7.0)", "babel (==2.9.0)"]
i18n = ["babel (>=2.9.0)"] i18n = ["babel (>=2.9.0)"]
min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"]
[[package]] [[package]]
name = "mkdocs-autorefs" name = "mkdocs-autorefs"
@@ -472,6 +483,9 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
[package.dependencies]
setuptools = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "21.3" version = "21.3"
@@ -500,8 +514,8 @@ optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[package.extras] [package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
@@ -535,14 +549,6 @@ pyyaml = ">=5.1"
toml = "*" toml = "*"
virtualenv = ">=20.0.8" virtualenv = ">=20.0.8"
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.13.0" version = "2.13.0"
@@ -574,11 +580,11 @@ optional = false
python-versions = ">=3.6.8" python-versions = ">=3.6.8"
[package.extras] [package.extras]
diagrams = ["railroad-diagrams", "jinja2"] diagrams = ["jinja2", "railroad-diagrams"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.1.3" version = "7.2.0"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev" category = "dev"
optional = false optional = false
@@ -587,12 +593,12 @@ python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
attrs = ">=19.2.0" attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""} colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*" iniconfig = "*"
packaging = "*" packaging = "*"
pluggy = ">=0.12,<2.0" pluggy = ">=0.12,<2.0"
py = ">=1.8.2" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
tomli = ">=1.0.0"
[package.extras] [package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
@@ -626,7 +632,7 @@ pytest = ">=6.1.0"
typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
[package.extras] [package.extras]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
[[package]] [[package]]
name = "pytest-cov" name = "pytest-cov"
@@ -642,7 +648,7 @@ pytest = ">=4.6"
toml = "*" toml = "*"
[package.extras] [package.extras]
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
@@ -690,7 +696,7 @@ urllib3 = ">=1.21.1,<1.27"
[package.extras] [package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]] [[package]]
name = "rich" name = "rich"
@@ -708,6 +714,19 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9
[package.extras] [package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[[package]]
name = "setuptools"
version = "65.5.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@@ -780,26 +799,26 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
[package.extras] [package.extras]
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.16.5" version = "20.16.6"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
distlib = ">=0.3.5,<1" distlib = ">=0.3.6,<1"
filelock = ">=3.4.1,<4" filelock = ">=3.4.1,<4"
importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""}
platformdirs = ">=2.4,<3" platformdirs = ">=2.4,<3"
[package.extras] [package.extras]
docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"]
testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
[[package]] [[package]]
@@ -828,15 +847,15 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[[package]] [[package]]
name = "zipp" name = "zipp"
version = "3.9.0" version = "3.10.0"
description = "Backport of pathlib-compatible object wrapper for zip files" description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[package.extras] [package.extras]
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[extras] [extras]
dev = ["aiohttp", "click", "msgpack"] dev = ["aiohttp", "click", "msgpack"]
@@ -844,10 +863,98 @@ dev = ["aiohttp", "click", "msgpack"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "84203bb5193474eb9204f4f808739cb25e61f02a38d0062ea2ea71d3703573c1" content-hash = "cfa35529900ee7fc7bca1e2a189f0240081bdbc75b501b25b394dfce66261c8b"
[metadata.files] [metadata.files]
aiohttp = [] aiohttp = [
{file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"},
{file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"},
{file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"},
{file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"},
{file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"},
{file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"},
{file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"},
{file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"},
{file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"},
{file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"},
{file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"},
{file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"},
{file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"},
{file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"},
{file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"},
{file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"},
{file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"},
{file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"},
{file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"},
{file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"},
{file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"},
{file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"},
{file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"},
{file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"},
{file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"},
{file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"},
{file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"},
]
aiosignal = [ aiosignal = [
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
@@ -860,43 +967,209 @@ asynctest = [
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
] ]
attrs = [] attrs = [
black = [] {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
]
black = [
{file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
{file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
{file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
{file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
{file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
{file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"},
{file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
{file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
{file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"},
{file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
{file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
{file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
{file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
{file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
{file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
{file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
{file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
{file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
{file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
{file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
]
cached-property = [ cached-property = [
{file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
{file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
] ]
certifi = [] certifi = [
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
]
cfgv = [ cfgv = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
] ]
charset-normalizer = [] charset-normalizer = [
{file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
{file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
]
click = [ click = [
{file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"}, {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
] ]
colorama = [ colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
colored = [
{file = "colored-1.4.3.tar.gz", hash = "sha256:b7b48b9f40e8a65bbb54813d5d79dd008dc8b8c5638d5bbfd30fc5a82e6def7a"},
] ]
colored = []
commonmark = [ commonmark = [
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
] ]
coverage = [] coverage = [
distlib = [] {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
filelock = [] {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
frozenlist = [] {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
{file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
{file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
{file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
{file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
{file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
{file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
{file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
{file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
{file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
{file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
{file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
{file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
{file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
{file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
{file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
{file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
{file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
{file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
{file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
{file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
{file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
{file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
{file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
{file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
{file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
{file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
{file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
{file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
{file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
{file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
{file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
{file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
{file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
{file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
{file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
{file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
{file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
{file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
{file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
{file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
{file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
{file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
{file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
{file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
{file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
{file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
{file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
]
distlib = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
exceptiongroup = [
{file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"},
{file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"},
]
filelock = [
{file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
{file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
]
frozenlist = [
{file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989"},
{file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204"},
{file = "frozenlist-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5"},
{file = "frozenlist-1.3.1-cp310-cp310-win32.whl", hash = "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be"},
{file = "frozenlist-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db"},
{file = "frozenlist-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c"},
{file = "frozenlist-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792"},
{file = "frozenlist-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc"},
{file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e"},
{file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519"},
{file = "frozenlist-1.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2"},
{file = "frozenlist-1.3.1-cp38-cp38-win32.whl", hash = "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845"},
{file = "frozenlist-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d"},
{file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1"},
{file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab"},
{file = "frozenlist-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc"},
{file = "frozenlist-1.3.1-cp39-cp39-win32.whl", hash = "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b"},
{file = "frozenlist-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189"},
{file = "frozenlist-1.3.1.tar.gz", hash = "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8"},
]
ghp-import = [ ghp-import = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
] ]
griffe = [] griffe = [
identify = [] {file = "griffe-0.23.0-py3-none-any.whl", hash = "sha256:cfca5f523808109da3f8cfaa46e325fa2e5bef51120d1146e908c121b56475f0"},
idna = [] {file = "griffe-0.23.0.tar.gz", hash = "sha256:a639e2968c8e27f56ebcc57f869a03cea7ac7e7f5684bd2429c665f761c4e7bd"},
importlib-metadata = [] ]
identify = [
{file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"},
{file = "identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"},
]
idna = [
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
importlib-metadata = [
{file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"},
{file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"},
]
iniconfig = [ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
@@ -955,13 +1228,22 @@ mergedeep = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
] ]
mkdocs = [] mkdocs = [
{file = "mkdocs-1.4.1-py3-none-any.whl", hash = "sha256:2b7845c2775396214cd408753e4cfb01af3cfed36acc141a84bce2ceec9d705d"},
{file = "mkdocs-1.4.1.tar.gz", hash = "sha256:07ed90be4062e4ef732bbac2623097b9dca35c67b562c38cfd0bfbc7151758c1"},
]
mkdocs-autorefs = [ mkdocs-autorefs = [
{file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
] ]
mkdocs-material = [] mkdocs-material = [
mkdocs-material-extensions = [] {file = "mkdocs_material-8.5.7-py3-none-any.whl", hash = "sha256:07fc70dfa325a8019b99a124751c43e4c1c2a739ed1b0b82c00f823f31c9a1e2"},
{file = "mkdocs_material-8.5.7.tar.gz", hash = "sha256:ff4c7851b2e5f9a6cfa0a8b247e973ebae753b9836a53bd68742827541ab73e5"},
]
mkdocs-material-extensions = [
{file = "mkdocs_material_extensions-1.1-py3-none-any.whl", hash = "sha256:bcc2e5fc70c0ec50e59703ee6e639d87c7e664c0c441c014ea84461a90f1e902"},
{file = "mkdocs_material_extensions-1.1.tar.gz", hash = "sha256:96ca979dae66d65c2099eefe189b49d5ac62f76afb59c38e069ffc7cf3c131ec"},
]
mkdocstrings = [ mkdocstrings = [
{file = "mkdocstrings-0.19.0-py3-none-any.whl", hash = "sha256:3217d510d385c961f69385a670b2677e68e07b5fea4a504d86bf54c006c87c7d"}, {file = "mkdocstrings-0.19.0-py3-none-any.whl", hash = "sha256:3217d510d385c961f69385a670b2677e68e07b5fea4a504d86bf54c006c87c7d"},
{file = "mkdocstrings-0.19.0.tar.gz", hash = "sha256:efa34a67bad11229d532d89f6836a8a215937548623b64f3698a1df62e01cc3e"}, {file = "mkdocstrings-0.19.0.tar.gz", hash = "sha256:efa34a67bad11229d532d89f6836a8a215937548623b64f3698a1df62e01cc3e"},
@@ -1085,12 +1367,40 @@ multidict = [
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
] ]
mypy = [] mypy = [
{file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"},
{file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"},
{file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"},
{file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"},
{file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"},
{file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"},
{file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"},
{file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"},
{file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"},
{file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"},
{file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"},
{file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"},
{file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"},
{file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"},
{file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"},
{file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"},
{file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"},
{file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"},
{file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"},
{file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"},
{file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"},
{file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"},
{file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"},
{file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"},
]
mypy-extensions = [ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
] ]
nanoid = [] nanoid = [
{file = "nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb"},
{file = "nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68"},
]
nodeenv = [ nodeenv = [
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
@@ -1099,7 +1409,10 @@ packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
] ]
pathspec = [] pathspec = [
{file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
]
platformdirs = [ platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
@@ -1108,23 +1421,34 @@ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
] ]
pre-commit = [] pre-commit = [
py = [ {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ]
pygments = [
{file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"},
{file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"},
]
pymdown-extensions = [
{file = "pymdown_extensions-9.7-py3-none-any.whl", hash = "sha256:767d07d9dead0f52f5135545c01f4ed627f9a7918ee86c646d893e24c59db87d"},
{file = "pymdown_extensions-9.7.tar.gz", hash = "sha256:651b0107bc9ee790aedea3673cb88832c0af27d2569cf45c2de06f1d65292e96"},
] ]
pygments = []
pymdown-extensions = []
pyparsing = [ pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
] ]
pytest = [] pytest = [
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
]
pytest-aiohttp = [ pytest-aiohttp = [
{file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"},
{file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"},
] ]
pytest-asyncio = [] pytest-asyncio = [
{file = "pytest-asyncio-0.20.1.tar.gz", hash = "sha256:626699de2a747611f3eeb64168b3575f70439b06c3d0206e6ceaeeb956e65519"},
{file = "pytest_asyncio-0.20.1-py3-none-any.whl", hash = "sha256:2c85a835df33fda40fe3973b451e0c194ca11bc2c007eabff90bb3d156fc172b"},
]
pytest-cov = [ pytest-cov = [
{file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
{file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"},
@@ -1141,6 +1465,13 @@ pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
@@ -1172,14 +1503,78 @@ pyyaml-env-tag = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
] ]
requests = [] requests = [
rich = [] {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
]
rich = [
{file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"},
{file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"},
]
setuptools = [
{file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
{file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"},
]
six = [ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
] ]
syrupy = [] syrupy = [
time-machine = [] {file = "syrupy-3.0.2-py3-none-any.whl", hash = "sha256:b7afb1424ddbdbfba6c7340fd7b939dd129879f891172dd2e59ec971fb40c60d"},
{file = "syrupy-3.0.2.tar.gz", hash = "sha256:5425a23f816743227542c91c83d2bf0847f029284236f8c30e10147bdde4f80e"},
]
time-machine = [
{file = "time-machine-2.8.2.tar.gz", hash = "sha256:2ff3cd145c381ac87b1c35400475a8f019b15dc2267861aad0466f55b49e7813"},
{file = "time_machine-2.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:931f762053031ec76e81d5b97b276d6cbc3c9958fd281a3661a4e4dcd434ae4d"},
{file = "time_machine-2.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2bec6756c46d9e7ccfaeb177fde46da01af74ac9e5862dd9528e501d367f451e"},
{file = "time_machine-2.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:959e63ad6980df1c36aefd19ae746e9b01c2be2f009199ec996fde0443b84de0"},
{file = "time_machine-2.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62db94b5ebe246949e6cedc57e7b96028f18ab9fb63b391d0e94d2e963702e30"},
{file = "time_machine-2.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4b40d872fd025c9ee6924372d345b2788aac9df89eba5562e6464dde04cf99"},
{file = "time_machine-2.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b68259837b59c3bef30c5cff24d73228c5a5821342af624c78707fe297153221"},
{file = "time_machine-2.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:46b4d2763c514d0036f7f46b23836d8fba0240ac1c50df588ca43193a59ee184"},
{file = "time_machine-2.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f416489bc8d0adb4bd63edcce5ba743b408f3c161ab0e1a65f9f904a6f9a06c0"},
{file = "time_machine-2.8.2-cp310-cp310-win32.whl", hash = "sha256:94ab54c2062a362059a02e6df624151bfdcda79dab704ffee220bb31f8153e24"},
{file = "time_machine-2.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:f227819cfa27793e759811dabe6187e8f36dba6ac3a404516e17a81bb0216763"},
{file = "time_machine-2.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:875eedfdf9cc59a9d119420b35c43a6d7ec08951a86581b4a4dbde47e6327256"},
{file = "time_machine-2.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01ee31fca1414d1198feff9eb7d062ca42aea9d1c01f63cdd6b2e0bb4f7479a9"},
{file = "time_machine-2.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4387678c392cfb40c038016b04f5becb022bdc371ecabded751c2a116d2c0b5a"},
{file = "time_machine-2.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a42739702fd8ccbf4295aa6a0e5089f0ce125974e06ab157c6e4f4eadbc167c"},
{file = "time_machine-2.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1963e1b9ea4891cbdd8a8f12cfb273dc7d3b0771ffe61238d688a7c2499445ef"},
{file = "time_machine-2.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7c0234c2fae05b4945b711d655af3487df34c466e184fbce7253dfc28c9980d1"},
{file = "time_machine-2.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19d01c6b6791c3ff45f8c82d149ac28292cf67242b1ace3dc1fdc0494edc111e"},
{file = "time_machine-2.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b05a2ca1045edd343fa07d2c55d57695c40b7af1e4c7df480d8e1976eb48a22f"},
{file = "time_machine-2.8.2-cp311-cp311-win32.whl", hash = "sha256:71607d92fd23cd5fc5bcddb3ec6b91a6a1b07f7277e7e58dce0a5c1f67d229cd"},
{file = "time_machine-2.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9e4c58915b2136041027fb4d795e8844112683e550a9aed24ecde1de8a5a8f2"},
{file = "time_machine-2.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b20f55d76cacb8b6f99c4161d8bfd6fc3be8d8ae003df2a79dbda9015d6ab85"},
{file = "time_machine-2.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb64b249df5c2958484706bdc095b326baf0f9c4a96c990d63a6e290680a8933"},
{file = "time_machine-2.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460f3d7344b64c906030013f6ca314017d7cbeb211e6c8c0efbdb3a2f5b168e3"},
{file = "time_machine-2.8.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ccd0e73e75f9cc624be08a2ae0305617ce7890d5b55f938ba336f086001ac66"},
{file = "time_machine-2.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8856b03574bc88f506534489562dfeb9c057485052817895413d8f33e7d03d28"},
{file = "time_machine-2.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3be539125dc815ff1f1ff05cd00f8839132a4b3a729809fa4a7de405f47cbd0b"},
{file = "time_machine-2.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c3b356e9038abb78618169b86a2bc3488aa2faee27fa97c9cd8638972d60dfe"},
{file = "time_machine-2.8.2-cp37-cp37m-win32.whl", hash = "sha256:bfbe53b80402ab3c93f112374d8624eb5e7f26395f01aea341bf91b4a512e36e"},
{file = "time_machine-2.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:71917d38d2c34039a31ac0d63970f6009072a14c3a89169d165ca81130daf308"},
{file = "time_machine-2.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a3384f03776ffed86afdc2a807aa80fc656fbce6605e9b89261fc17302759290"},
{file = "time_machine-2.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6d084ccfbf30c658c23b1340583aa64afe4c6421b4d2ab3a84769915630e0d68"},
{file = "time_machine-2.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ed6c02afa3fc48af1fa256d5a3a18b63c3e36e7759fec8184e340e1b2f38f77"},
{file = "time_machine-2.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c783769cc7b722e4b9df6015919a65952e58eb6fe884c198c1f56d58d883d0bc"},
{file = "time_machine-2.8.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da17b12c20d96b69bbe71d1e260e76c81072cded63539050d0f8aa26e9701dc"},
{file = "time_machine-2.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0213c32498190d7701cf90dc8a4f87d6d8571b856a16b474072e37f9e4daf896"},
{file = "time_machine-2.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c47caacc5a00656ee9e4ad4600ed46e036f233bbd93ed99c0da5f3dcec6a1a64"},
{file = "time_machine-2.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0e7950776b9087ba8e44f3602e5d695eaba853518c9963f41f3cba094000d87f"},
{file = "time_machine-2.8.2-cp38-cp38-win32.whl", hash = "sha256:8bb1e68434a6c45bf2ef5d738420399803e7aa8211d77353e416d5043f82053e"},
{file = "time_machine-2.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:f67957dac20cca1171a7b63a8343c86f4f589e42f3c61bce687e77dd475e4d88"},
{file = "time_machine-2.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:18d60cb6eb2bb896ef442628be783d2ddf374873caefb083cbc2b2ed19361157"},
{file = "time_machine-2.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:82055dc781c4c9f6c97f3a349473ab44f1096da61a8cf1e72c105d12a39344ea"},
{file = "time_machine-2.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfaa1018ea5695a47f9536e1c7f7a112d55741162d8cdaa49801b3977f710666"},
{file = "time_machine-2.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f9c6bdead992708d3f88e9e337f08f9067e259eb6a7df23f94652cee7f08459"},
{file = "time_machine-2.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e6ba08062248fd9ba750ca997ed8699176d71b0d3aa525333efbd10e644f574"},
{file = "time_machine-2.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7d7233bb7a01d27e93fd8f687227fb93d314fb5048127844c248d76067b36e84"},
{file = "time_machine-2.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0cb22588e0c88239bad7ac5d593dc1119aacb7ac074e7aa2badc53583b92febf"},
{file = "time_machine-2.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ba71634179448df5dc6fb85d61e3956c8e33755ad3f76549dacb9c4854e88046"},
{file = "time_machine-2.8.2-cp39-cp39-win32.whl", hash = "sha256:70ccbd8c5c4396fe4d60b0ceacef47f95e44f84a4d1d8cd5acdf9f81880e863a"},
{file = "time_machine-2.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:32f77a14ffbaeef8ae5e5bb86eb0e76057b56cb94f1f4990756c66047f8cac91"},
]
toml = [ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -1214,9 +1609,18 @@ typed-ast = [
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {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.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
] ]
typing-extensions = [] typing-extensions = [
urllib3 = [] {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
virtualenv = [] {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
]
urllib3 = [
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
]
virtualenv = [
{file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"},
{file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"},
]
watchdog = [ watchdog = [
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"}, {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"}, {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"},
@@ -1244,5 +1648,68 @@ watchdog = [
{file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"}, {file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"},
{file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"}, {file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"},
] ]
yarl = [] yarl = [
zipp = [] {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"},
{file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"},
{file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"},
{file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"},
{file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"},
{file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"},
{file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"},
{file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"},
{file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"},
{file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"},
{file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"},
{file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"},
{file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"},
{file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"},
{file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"},
{file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"},
{file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"},
{file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"},
{file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"},
]
zipp = [
{file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"},
{file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"},
]

View File

@@ -33,10 +33,10 @@ importlib-metadata = "^4.11.3"
typing-extensions = { version = "^4.0.0", python = "<3.10" } typing-extensions = { version = "^4.0.0", python = "<3.10" }
# Dependencies below are required for devtools only # Dependencies below are required for devtools only
aiohttp = { version = "^3.8.1", optional = true } aiohttp = { version = ">=3.8.1", optional = true }
click = {version = "8.1.2", optional = true} click = {version = ">=8.1.2", optional = true}
msgpack = { version = "^1.0.3", optional = true } msgpack = { version = ">=1.0.3", optional = true }
nanoid = "^2.0.0" nanoid = ">=2.0.0"
[tool.poetry.extras] [tool.poetry.extras]
dev = ["aiohttp", "click", "msgpack"] dev = ["aiohttp", "click", "msgpack"]

96
sandbox/will/fr.py Normal file
View File

@@ -0,0 +1,96 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Static
class StaticText(Static):
pass
class Header(Static):
pass
class Footer(Static):
pass
class FrApp(App):
CSS = """
Screen {
layout: horizontal;
align: center middle;
}
Vertical {
}
Header {
background: $boost;
content-align: center middle;
text-align: center;
color: $text;
height: 3;
border: tall $warning;
}
Horizontal {
height: 1fr;
align: center middle;
}
Footer {
background: $boost;
content-align: center middle;
text-align: center;
color: $text;
height: 6;
border: tall $warning;
}
StaticText {
background: $boost;
height: 8;
content-align: center middle;
text-align: center;
color: $text;
}
#foo {
width: 10;
border: tall $primary;
}
#bar {
width: 1fr;
border: tall $error;
}
#baz {
width: 20;
border: tall $success;
}
"""
def compose(self) -> ComposeResult:
yield Vertical(
Header("HEADER"),
Horizontal(
StaticText("foo", id="foo"),
StaticText("bar", id="bar"),
StaticText("baz", id="baz"),
),
Footer("FOOTER"),
)
app = FrApp()
app.run()

View File

@@ -51,7 +51,9 @@ class Logger:
try: try:
app = active_app.get() app = active_app.get()
except LookupError: except LookupError:
raise LoggerError("Unable to log without an active app.") from None print_args = (*args, *[f"{key}={value!r}" for key, value in kwargs.items()])
print(*print_args)
return
if app.devtools is None or not app.devtools.is_connected: if app.devtools is None or not app.devtools.is_connected:
return return

View File

@@ -60,10 +60,9 @@ def arrange(
for dock_widget in dock_widgets: for dock_widget in dock_widgets:
edge = dock_widget.styles.dock edge = dock_widget.styles.dock
fraction_unit = Fraction( box_model = dock_widget._get_box_model(
size.height if edge in ("top", "bottom") else size.width size, viewport, Fraction(size.width), Fraction(size.height)
) )
box_model = dock_widget._get_box_model(size, viewport, fraction_unit)
widget_width_fraction, widget_height_fraction, margin = box_model widget_width_fraction, widget_height_fraction, margin = box_model
widget_width = int(widget_width_fraction) + margin.width widget_width = int(widget_width_fraction) + margin.width

View File

@@ -5,6 +5,7 @@ import shlex
from typing import Iterable from typing import Iterable
from textual.app import App from textual.app import App
from textual.pilot import Pilot
from textual._import_app import import_app from textual._import_app import import_app
@@ -18,7 +19,7 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
path = cmd[0] path = cmd[0]
_press = attrs.get("press", None) _press = attrs.get("press", None)
press = [*_press.split(",")] if _press else ["_"] press = [*_press.split(",")] if _press else []
title = attrs.get("title") title = attrs.get("title")
print(f"screenshotting {path!r}") print(f"screenshotting {path!r}")
@@ -28,7 +29,7 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
rows = int(attrs.get("lines", 24)) rows = int(attrs.get("lines", 24))
columns = int(attrs.get("columns", 80)) columns = int(attrs.get("columns", 80))
svg = take_svg_screenshot( svg = take_svg_screenshot(
None, path, press, title, terminal_size=(rows, columns) None, path, press, title, terminal_size=(columns, rows)
) )
finally: finally:
os.chdir(cwd) os.chdir(cwd)
@@ -45,9 +46,9 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
def take_svg_screenshot( def take_svg_screenshot(
app: App | None = None, app: App | None = None,
app_path: str | None = None, app_path: str | None = None,
press: Iterable[str] = ("_",), press: Iterable[str] = (),
title: str | None = None, title: str | None = None,
terminal_size: tuple[int, int] = (24, 80), terminal_size: tuple[int, int] = (80, 24),
) -> str: ) -> str:
""" """
@@ -63,25 +64,29 @@ def take_svg_screenshot(
the screenshot was taken. the screenshot was taken.
""" """
rows, columns = terminal_size
os.environ["COLUMNS"] = str(columns)
os.environ["LINES"] = str(rows)
if app is None: if app is None:
assert app_path is not None
app = import_app(app_path) app = import_app(app_path)
assert app is not None
if title is None: if title is None:
title = app.title title = app.title
app.run( async def auto_pilot(pilot: Pilot) -> None:
quit_after=5, app = pilot.app
press=press or ["ctrl+c"], await pilot.press(*press)
svg = app.export_screenshot(title=title)
app.exit(svg)
svg = app.run(
headless=True, headless=True,
screenshot=True, auto_pilot=auto_pilot,
screenshot_title=title, size=terminal_size,
) )
svg = app._screenshot assert svg is not None
return svg return svg

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
import sys
import runpy import runpy
import shlex import shlex
from pathlib import Path from pathlib import Path
@@ -30,7 +31,6 @@ def import_app(import_name: str) -> App:
import inspect import inspect
import importlib import importlib
import sys
from textual.app import App, WINDOWS from textual.app import App, WINDOWS
@@ -45,8 +45,7 @@ def import_app(import_name: str) -> App:
except Exception as error: except Exception as error:
raise AppFail(str(error)) raise AppFail(str(error))
if "sys" in global_vars: sys.argv[:] = [path, *argv]
global_vars["sys"].argv = [path, *argv]
if name: if name:
# User has given a name, use that # User has given a name, use that

View File

@@ -1,12 +1,23 @@
from __future__ import annotations from __future__ import annotations
import sys
from fractions import Fraction from fractions import Fraction
from itertools import accumulate from itertools import accumulate
from typing import cast, Sequence from typing import cast, Sequence, TYPE_CHECKING
from .box_model import BoxModel
from .css.scalar import Scalar from .css.scalar import Scalar
from .geometry import Size from .geometry import Size
if TYPE_CHECKING:
from .widget import Widget
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
def resolve( def resolve(
dimensions: Sequence[Scalar], dimensions: Sequence[Scalar],
@@ -71,3 +82,79 @@ def resolve(
] ]
return results return results
def resolve_box_models(
dimensions: list[Scalar | None],
widgets: list[Widget],
size: Size,
parent_size: Size,
dimension: Literal["width", "height"] = "width",
) -> list[BoxModel]:
"""Resolve box models for a list of dimensions
Args:
dimensions (list[Scalar | None]): A list of Scalars or Nones for each dimension.
widgets (list[Widget]): Widgets in resolve.
size (Size): size of container.
parent_size (Size): Size of parent.
dimensions (Literal["width", "height"]): Which dimension to resolve.
Returns:
list[BoxModel]: List of resolved box models.
"""
fraction_width = Fraction(size.width)
fraction_height = Fraction(size.height)
box_models: list[BoxModel | None] = [
(
None
if dimension is not None and dimension.is_fraction
else widget._get_box_model(
size, parent_size, fraction_width, fraction_height
)
)
for (dimension, widget) in zip(dimensions, widgets)
]
if dimension == "width":
total_remaining = sum(
box_model.width for box_model in box_models if box_model is not None
)
remaining_space = max(0, size.width - total_remaining)
else:
total_remaining = sum(
box_model.height for box_model in box_models if box_model is not None
)
remaining_space = max(0, size.height - total_remaining)
fraction_unit = Fraction(
remaining_space,
int(
sum(
dimension.value
for dimension in dimensions
if dimension and dimension.is_fraction
)
)
or 1,
)
if dimension == "width":
width_fraction = fraction_unit
height_fraction = Fraction(size.height)
else:
width_fraction = Fraction(size.width)
height_fraction = fraction_unit
box_models = [
box_model
or widget._get_box_model(
size,
parent_size,
width_fraction,
height_fraction,
)
for widget, box_model in zip(widgets, box_models)
]
return cast("list[BoxModel]", box_models)

View File

@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import Task
from contextlib import asynccontextmanager
import inspect import inspect
import io import io
import os import os
@@ -12,7 +14,18 @@ from contextlib import redirect_stderr, redirect_stdout
from datetime import datetime from datetime import datetime
from pathlib import Path, PurePath from pathlib import Path, PurePath
from time import perf_counter from time import perf_counter
from typing import Any, Generic, Iterable, Type, TYPE_CHECKING, TypeVar, cast, Union from typing import (
Any,
Callable,
Coroutine,
Generic,
Iterable,
Type,
TYPE_CHECKING,
TypeVar,
cast,
Union,
)
from weakref import WeakSet, WeakValueDictionary from weakref import WeakSet, WeakValueDictionary
from ._ansi_sequences import SYNC_END, SYNC_START from ._ansi_sequences import SYNC_END, SYNC_START
@@ -51,7 +64,12 @@ from .widget import AwaitMount, Widget
if TYPE_CHECKING: if TYPE_CHECKING:
from .devtools.client import DevtoolsClient from .devtools.client import DevtoolsClient
from .pilot import Pilot
if sys.version_info >= (3, 10):
from typing import TypeAlias
else: # pragma: no cover
from typing_extensions import TypeAlias
PLATFORM = platform.system() PLATFORM = platform.system()
WINDOWS = PLATFORM == "Windows" WINDOWS = PLATFORM == "Windows"
@@ -89,6 +107,9 @@ ComposeResult = Iterable[Widget]
RenderResult = RenderableType RenderResult = RenderableType
AutopilotCallbackType: TypeAlias = "Callable[[Pilot], Coroutine[Any, Any, None]]"
class AppError(Exception): class AppError(Exception):
pass pass
@@ -170,7 +191,7 @@ class App(Generic[ReturnType], DOMNode):
if no_color is not None: if no_color is not None:
self._filter = Monochrome() self._filter = Monochrome()
self.console = Console( self.console = Console(
file=(_NullFile() if self.is_headless else sys.__stdout__), file=sys.__stdout__ if sys.__stdout__ is not None else _NullFile(),
markup=False, markup=False,
highlight=False, highlight=False,
emoji=False, emoji=False,
@@ -241,6 +262,11 @@ class App(Generic[ReturnType], DOMNode):
) )
self._screenshot: str | None = None self._screenshot: str | None = None
@property
def return_value(self) -> ReturnType | None:
"""Get the return type."""
return self._return_value
def animate( def animate(
self, self,
attribute: str, attribute: str,
@@ -295,7 +321,7 @@ class App(Generic[ReturnType], DOMNode):
bool: True if the app is in headless mode. bool: True if the app is in headless mode.
""" """
return "headless" in self.features return False if self._driver is None else self._driver.is_headless
@property @property
def screen_stack(self) -> list[Screen]: def screen_stack(self) -> list[Screen]:
@@ -314,7 +340,7 @@ class App(Generic[ReturnType], DOMNode):
result (ReturnType | None, optional): Return value. Defaults to None. result (ReturnType | None, optional): Return value. Defaults to None.
""" """
self._return_value = result self._return_value = result
self._close_messages_no_wait() self.post_message_no_wait(messages.ExitApp(sender=self))
@property @property
def focused(self) -> Widget | None: def focused(self) -> Widget | None:
@@ -418,7 +444,11 @@ class App(Generic[ReturnType], DOMNode):
Returns: Returns:
Size: Size of the terminal Size: Size of the terminal
""" """
return Size(*self.console.size) if self._driver is not None and self._driver._size is not None:
width, height = self._driver._size
else:
width, height = self.console.size
return Size(width, height)
@property @property
def log(self) -> Logger: def log(self) -> Logger:
@@ -500,10 +530,11 @@ class App(Generic[ReturnType], DOMNode):
to use app title. Defaults to None. to use app title. Defaults to None.
""" """
assert self._driver is not None, "App must be running"
width, height = self.size
console = Console( console = Console(
width=self.console.width, width=width,
height=self.console.height, height=height,
file=io.StringIO(), file=io.StringIO(),
force_terminal=True, force_terminal=True,
color_system="truecolor", color_system="truecolor",
@@ -567,95 +598,170 @@ class App(Generic[ReturnType], DOMNode):
keys, action, description, show=show, key_display=key_display keys, action, description, show=show, key_display=key_display
) )
async def _press_keys(self, keys: Iterable[str]) -> None:
"""A task to send key events."""
app = self
driver = app._driver
assert driver is not None
await asyncio.sleep(0.02)
for key in keys:
if key == "_":
print("(pause 50ms)")
await asyncio.sleep(0.05)
elif key.startswith("wait:"):
_, wait_ms = key.split(":")
print(f"(pause {wait_ms}ms)")
await asyncio.sleep(float(wait_ms) / 1000)
else:
if len(key) == 1 and not key.isalnum():
key = (
unicodedata.name(key)
.lower()
.replace("-", "_")
.replace(" ", "_")
)
original_key = REPLACED_KEYS.get(key, key)
char: str | None
try:
char = unicodedata.lookup(original_key.upper().replace("_", " "))
except KeyError:
char = key if len(key) == 1 else None
print(f"press {key!r} (char={char!r})")
key_event = events.Key(app, key, char)
driver.send_event(key_event)
# TODO: A bit of a fudge - extra sleep after tabbing to help guard against race
# condition between widget-level key handling and app/screen level handling.
# More information here: https://github.com/Textualize/textual/issues/1009
# This conditional sleep can be removed after that issue is closed.
if key == "tab":
await asyncio.sleep(0.05)
await asyncio.sleep(0.02)
await app._animator.wait_for_idle()
@asynccontextmanager
async def run_test(
self,
*,
headless: bool = True,
size: tuple[int, int] | None = (80, 24),
):
"""An asynchronous context manager for testing app.
Args:
headless (bool, optional): Run in headless mode (no output or input). Defaults to True.
size (tuple[int, int] | None, optional): Force terminal size to `(WIDTH, HEIGHT)`,
or None to auto-detect. Defaults to None.
"""
from .pilot import Pilot
app = self
app_ready_event = asyncio.Event()
def on_app_ready() -> None:
"""Called when app is ready to process events."""
app_ready_event.set()
async def run_app(app) -> None:
await app._process_messages(
ready_callback=on_app_ready,
headless=headless,
terminal_size=size,
)
# Launch the app in the "background"
app_task = asyncio.create_task(run_app(app))
# Wait until the app has performed all startup routines.
await app_ready_event.wait()
# Context manager returns pilot object to manipulate the app
yield Pilot(app)
# Shutdown the app cleanly
await app._shutdown()
await app_task
async def run_async(
self,
*,
headless: bool = False,
size: tuple[int, int] | None = None,
auto_pilot: AutopilotCallbackType | None = None,
) -> ReturnType | None:
"""Run the app asynchronously.
Args:
headless (bool, optional): Run in headless mode (no output). Defaults to False.
size (tuple[int, int] | None, optional): Force terminal size to `(WIDTH, HEIGHT)`,
or None to auto-detect. Defaults to None.
auto_pilot (AutopilotCallbackType): An auto pilot coroutine.
Returns:
ReturnType | None: App return value.
"""
from .pilot import Pilot
app = self
auto_pilot_task: Task | None = None
async def app_ready() -> None:
"""Called by the message loop when the app is ready."""
nonlocal auto_pilot_task
if auto_pilot is not None:
async def run_auto_pilot(
auto_pilot: AutopilotCallbackType, pilot: Pilot
) -> None:
try:
await auto_pilot(pilot)
except Exception:
app.exit()
raise
pilot = Pilot(app)
auto_pilot_task = asyncio.create_task(run_auto_pilot(auto_pilot, pilot))
try:
await app._process_messages(
ready_callback=None if auto_pilot is None else app_ready,
headless=headless,
terminal_size=size,
)
finally:
if auto_pilot_task is not None:
await auto_pilot_task
await app._shutdown()
return app.return_value
def run( def run(
self, self,
*, *,
quit_after: float | None = None,
headless: bool = False, headless: bool = False,
press: Iterable[str] | None = None, size: tuple[int, int] | None = None,
screenshot: bool = False, auto_pilot: AutopilotCallbackType | None = None,
screenshot_title: str | None = None,
) -> ReturnType | None: ) -> ReturnType | None:
"""The main entry point for apps. """Run the app.
Args: Args:
quit_after (float | None, optional): Quit after a given number of seconds, or None headless (bool, optional): Run in headless mode (no output). Defaults to False.
to run forever. Defaults to None. size (tuple[int, int] | None, optional): Force terminal size to `(WIDTH, HEIGHT)`,
headless (bool, optional): Run in "headless" mode (don't write to stdout). or None to auto-detect. Defaults to None.
press (str, optional): An iterable of keys to simulate being pressed. auto_pilot (AutopilotCallbackType): An auto pilot coroutine.
screenshot (bool, optional): Take a screenshot after pressing keys (svg data stored in self._screenshot). Defaults to False.
screenshot_title (str | None, optional): Title of screenshot, or None to use App title. Defaults to None.
Returns: Returns:
ReturnType | None: The return value specified in `App.exit` or None if exit wasn't called. ReturnType | None: App return value.
""" """
if headless:
self.features = cast(
"frozenset[FeatureFlag]", self.features.union({"headless"})
)
async def run_app() -> None: async def run_app() -> None:
if quit_after is not None: """Run the app."""
self.set_timer(quit_after, self.shutdown) await self.run_async(
if press is not None: headless=headless,
app = self size=size,
auto_pilot=auto_pilot,
async def press_keys() -> None: )
"""A task to send key events."""
assert press
driver = app._driver
assert driver is not None
await asyncio.sleep(0.02)
for key in press:
if key == "_":
print("(pause 50ms)")
await asyncio.sleep(0.05)
elif key.startswith("wait:"):
_, wait_ms = key.split(":")
print(f"(pause {wait_ms}ms)")
await asyncio.sleep(float(wait_ms) / 1000)
else:
if len(key) == 1 and not key.isalnum():
key = (
unicodedata.name(key)
.lower()
.replace("-", "_")
.replace(" ", "_")
)
original_key = REPLACED_KEYS.get(key, key)
try:
char = unicodedata.lookup(
original_key.upper().replace("_", " ")
)
except KeyError:
char = key if len(key) == 1 else None
print(f"press {key!r} (char={char!r})")
key_event = events.Key(self, key, char)
driver.send_event(key_event)
# TODO: A bit of a fudge - extra sleep after tabbing to help guard against race
# condition between widget-level key handling and app/screen level handling.
# More information here: https://github.com/Textualize/textual/issues/1009
# This conditional sleep can be removed after that issue is closed.
if key == "tab":
await asyncio.sleep(0.05)
await asyncio.sleep(0.02)
await app._animator.wait_for_idle()
if screenshot:
self._screenshot = self.export_screenshot(
title=screenshot_title
)
await self.shutdown()
async def press_keys_task():
"""Press some keys in the background."""
asyncio.create_task(press_keys())
await self._process_messages(ready_callback=press_keys_task)
else:
await self._process_messages()
if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED: if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED:
# N.B. This doesn't work with Python<3.10, as we end up with 2 event loops: # N.B. This doesn't work with Python<3.10, as we end up with 2 event loops:
@@ -664,8 +770,7 @@ class App(Generic[ReturnType], DOMNode):
# However, this works with Python<3.10: # However, this works with Python<3.10:
event_loop = asyncio.get_event_loop() event_loop = asyncio.get_event_loop()
event_loop.run_until_complete(run_app()) event_loop.run_until_complete(run_app())
return self.return_value
return self._return_value
async def _on_css_change(self) -> None: async def _on_css_change(self) -> None:
"""Called when the CSS changes (if watch_css is True).""" """Called when the CSS changes (if watch_css is True)."""
@@ -756,8 +861,6 @@ class App(Generic[ReturnType], DOMNode):
def get_screen(self, screen: Screen | str) -> Screen: def get_screen(self, screen: Screen | str) -> Screen:
"""Get an installed screen. """Get an installed screen.
If the screen isn't running, it will be registered before it is run.
Args: Args:
screen (Screen | str): Either a Screen object or screen name (the `name` argument when installed). screen (Screen | str): Either a Screen object or screen name (the `name` argument when installed).
@@ -774,10 +877,30 @@ class App(Generic[ReturnType], DOMNode):
raise KeyError(f"No screen called {screen!r} installed") from None raise KeyError(f"No screen called {screen!r} installed") from None
else: else:
next_screen = screen next_screen = screen
if not next_screen.is_running:
self._register(self, next_screen)
return next_screen return next_screen
def _get_screen(self, screen: Screen | str) -> tuple[Screen, AwaitMount]:
"""Get an installed screen and a await mount object.
If the screen isn't running, it will be registered before it is run.
Args:
screen (Screen | str): Either a Screen object or screen name (the `name` argument when installed).
Raises:
KeyError: If the named screen doesn't exist.
Returns:
tuple[Screen, AwaitMount]: A screen instance and an awaitable that awaits the children mounting.
"""
_screen = self.get_screen(screen)
if not _screen.is_running:
widgets = self._register(self, _screen)
return (_screen, AwaitMount(widgets))
else:
return (_screen, AwaitMount([]))
def _replace_screen(self, screen: Screen) -> Screen: def _replace_screen(self, screen: Screen) -> Screen:
"""Handle the replaced screen. """Handle the replaced screen.
@@ -795,19 +918,20 @@ class App(Generic[ReturnType], DOMNode):
self.log.system(f"{screen} REMOVED") self.log.system(f"{screen} REMOVED")
return screen return screen
def push_screen(self, screen: Screen | str) -> None: def push_screen(self, screen: Screen | str) -> AwaitMount:
"""Push a new screen on the screen stack. """Push a new screen on the screen stack.
Args: Args:
screen (Screen | str): A Screen instance or the name of an installed screen. screen (Screen | str): A Screen instance or the name of an installed screen.
""" """
next_screen = self.get_screen(screen) next_screen, await_mount = self._get_screen(screen)
self._screen_stack.append(next_screen) self._screen_stack.append(next_screen)
self.screen.post_message_no_wait(events.ScreenResume(self)) self.screen.post_message_no_wait(events.ScreenResume(self))
self.log.system(f"{self.screen} is current (PUSHED)") self.log.system(f"{self.screen} is current (PUSHED)")
return await_mount
def switch_screen(self, screen: Screen | str) -> None: def switch_screen(self, screen: Screen | str) -> AwaitMount:
"""Switch to another screen by replacing the top of the screen stack with a new screen. """Switch to another screen by replacing the top of the screen stack with a new screen.
Args: Args:
@@ -816,12 +940,14 @@ class App(Generic[ReturnType], DOMNode):
""" """
if self.screen is not screen: if self.screen is not screen:
self._replace_screen(self._screen_stack.pop()) self._replace_screen(self._screen_stack.pop())
next_screen = self.get_screen(screen) next_screen, await_mount = self._get_screen(screen)
self._screen_stack.append(next_screen) self._screen_stack.append(next_screen)
self.screen.post_message_no_wait(events.ScreenResume(self)) self.screen.post_message_no_wait(events.ScreenResume(self))
self.log.system(f"{self.screen} is current (SWITCHED)") self.log.system(f"{self.screen} is current (SWITCHED)")
return await_mount
return AwaitMount([])
def install_screen(self, screen: Screen, name: str | None = None) -> str: def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount:
"""Install a screen. """Install a screen.
Args: Args:
@@ -833,7 +959,7 @@ class App(Generic[ReturnType], DOMNode):
ScreenError: If the screen can't be installed. ScreenError: If the screen can't be installed.
Returns: Returns:
str: The name of the screen AwaitMount: An awaitable that awaits the mounting of the screen and its children.
""" """
if name is None: if name is None:
name = nanoid.generate() name = nanoid.generate()
@@ -844,9 +970,9 @@ class App(Generic[ReturnType], DOMNode):
"Can't install screen; {screen!r} has already been installed" "Can't install screen; {screen!r} has already been installed"
) )
self._installed_screens[name] = screen self._installed_screens[name] = screen
self.get_screen(name) # Ensures screen is running _screen, await_mount = self._get_screen(name) # Ensures screen is running
self.log.system(f"{screen} INSTALLED name={name!r}") self.log.system(f"{screen} INSTALLED name={name!r}")
return name return await_mount
def uninstall_screen(self, screen: Screen | str) -> str | None: def uninstall_screen(self, screen: Screen | str) -> str | None:
"""Uninstall a screen. If the screen was not previously installed then this """Uninstall a screen. If the screen was not previously installed then this
@@ -992,7 +1118,10 @@ class App(Generic[ReturnType], DOMNode):
self._exit_renderables.clear() self._exit_renderables.clear()
async def _process_messages( async def _process_messages(
self, ready_callback: CallbackType | None = None self,
ready_callback: CallbackType | None = None,
headless: bool = False,
terminal_size: tuple[int, int] | None = None,
) -> None: ) -> None:
self._set_active() self._set_active()
@@ -1038,22 +1167,31 @@ class App(Generic[ReturnType], DOMNode):
self.log.system("[b green]STARTED[/]", self.css_monitor) self.log.system("[b green]STARTED[/]", self.css_monitor)
async def run_process_messages(): async def run_process_messages():
"""The main message loop, invoke below."""
async def invoke_ready_callback() -> None:
if ready_callback is not None:
ready_result = ready_callback()
if inspect.isawaitable(ready_result):
await ready_result
try: try:
await self._dispatch_message(events.Compose(sender=self)) try:
await self._dispatch_message(events.Mount(sender=self)) await self._dispatch_message(events.Compose(sender=self))
await self._dispatch_message(events.Mount(sender=self))
finally:
self._mounted_event.set()
Reactive._initialize_object(self)
self.stylesheet.update(self)
self.refresh()
await self.animator.start()
finally: finally:
self._mounted_event.set() await self._ready()
await invoke_ready_callback()
Reactive._initialize_object(self)
self.stylesheet.update(self)
self.refresh()
await self.animator.start()
await self._ready()
if ready_callback is not None:
await ready_callback()
self._running = True self._running = True
@@ -1067,7 +1205,6 @@ class App(Generic[ReturnType], DOMNode):
await timer.stop() await timer.stop()
await self.animator.stop() await self.animator.stop()
await self._close_all()
self._running = True self._running = True
try: try:
@@ -1077,13 +1214,13 @@ class App(Generic[ReturnType], DOMNode):
driver: Driver driver: Driver
driver_class = cast( driver_class = cast(
"type[Driver]", "type[Driver]",
HeadlessDriver if self.is_headless else self.driver_class, HeadlessDriver if headless else self.driver_class,
) )
driver = self._driver = driver_class(self.console, self) driver = self._driver = driver_class(self.console, self, size=terminal_size)
driver.start_application_mode() driver.start_application_mode()
try: try:
if self.is_headless: if headless:
await run_process_messages() await run_process_messages()
else: else:
if self.devtools is not None: if self.devtools is not None:
@@ -1105,11 +1242,6 @@ class App(Generic[ReturnType], DOMNode):
driver.stop_application_mode() driver.stop_application_mode()
except Exception as error: except Exception as error:
self._handle_exception(error) self._handle_exception(error)
finally:
self._running = False
self._print_error_renderables()
if self.devtools is not None and self.devtools.is_connected:
await self._disconnect_devtools()
async def _pre_process(self) -> None: async def _pre_process(self) -> None:
pass pass
@@ -1134,12 +1266,17 @@ class App(Generic[ReturnType], DOMNode):
"""Used by docs plugin.""" """Used by docs plugin."""
svg = self.export_screenshot(title=screenshot_title) svg = self.export_screenshot(title=screenshot_title)
self._screenshot = svg # type: ignore self._screenshot = svg # type: ignore
await self.shutdown() self.exit()
self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer") self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer")
async def _on_compose(self) -> None: async def _on_compose(self) -> None:
widgets = list(self.compose()) try:
widgets = list(self.compose())
except TypeError as error:
raise TypeError(
f"{self!r} compose() returned an invalid response; {error}"
) from None
await self.mount_all(widgets) await self.mount_all(widgets)
def _on_idle(self) -> None: def _on_idle(self) -> None:
@@ -1218,8 +1355,10 @@ class App(Generic[ReturnType], DOMNode):
parent (Widget): The parent of the Widget. parent (Widget): The parent of the Widget.
widget (Widget): The Widget to start. widget (Widget): The Widget to start.
""" """
widget._attach(parent) widget._attach(parent)
widget._start_messages() widget._start_messages()
self.app._registry.add(widget)
def is_mounted(self, widget: Widget) -> bool: def is_mounted(self, widget: Widget) -> bool:
"""Check if a widget is mounted. """Check if a widget is mounted.
@@ -1233,17 +1372,43 @@ class App(Generic[ReturnType], DOMNode):
return widget in self._registry return widget in self._registry
async def _close_all(self) -> None: async def _close_all(self) -> None:
while self._registry: """Close all message pumps."""
child = self._registry.pop()
# Close all screens on the stack
for screen in self._screen_stack:
if screen._running:
await self._prune_node(screen)
self._screen_stack.clear()
# Close pre-defined screens
for screen in self.SCREENS.values():
if screen._running:
await self._prune_node(screen)
# Close any remaining nodes
# Should be empty by now
remaining_nodes = list(self._registry)
for child in remaining_nodes:
await child._close_messages() await child._close_messages()
async def shutdown(self): async def _shutdown(self) -> None:
await self._disconnect_devtools()
driver = self._driver driver = self._driver
self._running = False
if driver is not None: if driver is not None:
driver.disable_input() driver.disable_input()
await self._close_all()
await self._close_messages() await self._close_messages()
await self._dispatch_message(events.Unmount(sender=self))
self._print_error_renderables()
if self.devtools is not None and self.devtools.is_connected:
await self._disconnect_devtools()
async def _on_exit_app(self) -> None:
await self._message_queue.put(None)
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None: def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
if self._screen_stack: if self._screen_stack:
self.screen.refresh(repaint=repaint, layout=layout) self.screen.refresh(repaint=repaint, layout=layout)
@@ -1497,18 +1662,61 @@ class App(Generic[ReturnType], DOMNode):
[to_remove for to_remove in remove_widgets if to_remove.can_focus], [to_remove for to_remove in remove_widgets if to_remove.can_focus],
) )
for child in remove_widgets: await self._prune_node(widget)
await child._close_messages()
self._unregister(child)
if parent is not None: if parent is not None:
parent.refresh(layout=True) parent.refresh(layout=True)
def _walk_children(self, root: Widget) -> Iterable[list[Widget]]:
"""Walk children depth first, generating widgets and a list of their siblings.
Returns:
Iterable[list[Widget]]: The child widgets of root.
"""
stack: list[Widget] = [root]
pop = stack.pop
push = stack.append
while stack:
widget = pop()
if widget.children:
yield [*widget.children, *widget._get_virtual_dom()]
for child in widget.children:
push(child)
async def _prune_node(self, root: Widget) -> None:
"""Remove a node and its children. Children are removed before parents.
Args:
root (Widget): Node to remove.
"""
# Pruning a node that has been removed is a no-op
if root not in self._registry:
return
node_children = list(self._walk_children(root))
for children in reversed(node_children):
# Closing children can be done asynchronously.
close_messages = [
child._close_messages() for child in children if child._running
]
# TODO: What if a message pump refuses to exit?
if close_messages:
await asyncio.gather(*close_messages)
for child in children:
self._unregister(child)
await root._close_messages()
self._unregister(root)
async def action_check_bindings(self, key: str) -> None: async def action_check_bindings(self, key: str) -> None:
await self.check_bindings(key) await self.check_bindings(key)
async def action_quit(self) -> None: async def action_quit(self) -> None:
"""Quit the app as soon as possible.""" """Quit the app as soon as possible."""
await self.shutdown() self.exit()
async def action_bang(self) -> None: async def action_bang(self) -> None:
1 / 0 1 / 0

View File

@@ -20,7 +20,8 @@ def get_box_model(
styles: StylesBase, styles: StylesBase,
container: Size, container: Size,
viewport: Size, viewport: Size,
fraction_unit: Fraction, width_fraction: Fraction,
height_fraction: Fraction,
get_content_width: Callable[[Size, Size], int], get_content_width: Callable[[Size, Size], int],
get_content_height: Callable[[Size, Size, int], int], get_content_height: Callable[[Size, Size, int], int],
) -> BoxModel: ) -> BoxModel:
@@ -30,6 +31,8 @@ def get_box_model(
styles (StylesBase): Styles object. styles (StylesBase): Styles object.
container (Size): The size of the widget container. container (Size): The size of the widget container.
viewport (Size): The viewport size. viewport (Size): The viewport size.
width_fraction (Fraction): A fraction used for 1 `fr` unit on the width dimension.
height_fraction (Fraction):A fraction used for 1 `fr` unit on the height dimension.
get_auto_width (Callable): A callable which accepts container size and parent size and returns a width. get_auto_width (Callable): A callable which accepts container size and parent size and returns a width.
get_auto_height (Callable): A callable which accepts container size and parent size and returns a height. get_auto_height (Callable): A callable which accepts container size and parent size and returns a height.
@@ -63,7 +66,7 @@ def get_box_model(
# An explicit width # An explicit width
styles_width = styles.width styles_width = styles.width
content_width = styles_width.resolve_dimension( content_width = styles_width.resolve_dimension(
sizing_container - styles.margin.totals, viewport, fraction_unit sizing_container - styles.margin.totals, viewport, width_fraction
) )
if is_border_box and styles_width.excludes_border: if is_border_box and styles_width.excludes_border:
content_width -= gutter.width content_width -= gutter.width
@@ -71,14 +74,14 @@ def get_box_model(
if styles.min_width is not None: if styles.min_width is not None:
# Restrict to minimum width, if set # Restrict to minimum width, if set
min_width = styles.min_width.resolve_dimension( min_width = styles.min_width.resolve_dimension(
content_container, viewport, fraction_unit content_container, viewport, width_fraction
) )
content_width = max(content_width, min_width) content_width = max(content_width, min_width)
if styles.max_width is not None: if styles.max_width is not None:
# Restrict to maximum width, if set # Restrict to maximum width, if set
max_width = styles.max_width.resolve_dimension( max_width = styles.max_width.resolve_dimension(
content_container, viewport, fraction_unit content_container, viewport, width_fraction
) )
if is_border_box: if is_border_box:
max_width -= gutter.width max_width -= gutter.width
@@ -98,7 +101,7 @@ def get_box_model(
styles_height = styles.height styles_height = styles.height
# Explicit height set # Explicit height set
content_height = styles_height.resolve_dimension( content_height = styles_height.resolve_dimension(
sizing_container - styles.margin.totals, viewport, fraction_unit sizing_container - styles.margin.totals, viewport, height_fraction
) )
if is_border_box and styles_height.excludes_border: if is_border_box and styles_height.excludes_border:
content_height -= gutter.height content_height -= gutter.height
@@ -106,14 +109,14 @@ def get_box_model(
if styles.min_height is not None: if styles.min_height is not None:
# Restrict to minimum height, if set # Restrict to minimum height, if set
min_height = styles.min_height.resolve_dimension( min_height = styles.min_height.resolve_dimension(
content_container, viewport, fraction_unit content_container, viewport, height_fraction
) )
content_height = max(content_height, min_height) content_height = max(content_height, min_height)
if styles.max_height is not None: if styles.max_height is not None:
# Restrict maximum height, if set # Restrict maximum height, if set
max_height = styles.max_height.resolve_dimension( max_height = styles.max_height.resolve_dimension(
content_container, viewport, fraction_unit content_container, viewport, height_fraction
) )
content_height = min(content_height, max_height) content_height = min(content_height, max_height)

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import click import click
from importlib_metadata import version from importlib_metadata import version
from textual.pilot import Pilot
from textual._import_app import import_app, AppFail from textual._import_app import import_app, AppFail
@@ -84,7 +85,12 @@ def run_app(import_name: str, dev: bool, press: str) -> None:
sys.exit(1) sys.exit(1)
press_keys = press.split(",") if press else None press_keys = press.split(",") if press else None
result = app.run(press=press_keys)
async def run_press_keys(pilot: Pilot) -> None:
if press_keys is not None:
await pilot.press(*press_keys)
result = app.run(auto_pilot=run_press_keys)
if result is not None: if result is not None:
from rich.console import Console from rich.console import Console

View File

@@ -114,13 +114,14 @@ DarkSwitch {
} }
DarkSwitch .label { DarkSwitch .label {
width: 1fr;
padding: 1 2; padding: 1 2;
color: $text-muted; color: $text-muted;
} }
DarkSwitch Checkbox { DarkSwitch Checkbox {
background: $boost; background: $boost;
dock: left;
} }

View File

@@ -13,14 +13,25 @@ if TYPE_CHECKING:
class Driver(ABC): class Driver(ABC):
def __init__( def __init__(
self, console: "Console", target: "MessageTarget", debug: bool = False self,
console: "Console",
target: "MessageTarget",
*,
debug: bool = False,
size: tuple[int, int] | None = None,
) -> None: ) -> None:
self.console = console self.console = console
self._target = target self._target = target
self._debug = debug self._debug = debug
self._size = size
self._loop = asyncio.get_running_loop() self._loop = asyncio.get_running_loop()
self._mouse_down_time = _clock.get_time_no_wait() self._mouse_down_time = _clock.get_time_no_wait()
@property
def is_headless(self) -> bool:
"""Check if the driver is 'headless'"""
return False
def send_event(self, event: events.Event) -> None: def send_event(self, event: events.Event) -> None:
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self._target.post_message(event), loop=self._loop self._target.post_message(event), loop=self._loop

View File

@@ -9,7 +9,13 @@ from .. import events
class HeadlessDriver(Driver): class HeadlessDriver(Driver):
"""A do-nothing driver for testing.""" """A do-nothing driver for testing."""
@property
def is_headless(self) -> bool:
return True
def _get_terminal_size(self) -> tuple[int, int]: def _get_terminal_size(self) -> tuple[int, int]:
if self._size is not None:
return self._size
width: int | None = 80 width: int | None = 80
height: int | None = 25 height: int | None = 25
import shutil import shutil

View File

@@ -30,9 +30,14 @@ class LinuxDriver(Driver):
"""Powers display and input for Linux / MacOS""" """Powers display and input for Linux / MacOS"""
def __init__( def __init__(
self, console: "Console", target: "MessageTarget", debug: bool = False self,
console: "Console",
target: "MessageTarget",
*,
debug: bool = False,
size: tuple[int, int] | None = None,
) -> None: ) -> None:
super().__init__(console, target, debug) super().__init__(console, target, debug=debug, size=size)
self.fileno = sys.stdin.fileno() self.fileno = sys.stdin.fileno()
self.attrs_before: list[Any] | None = None self.attrs_before: list[Any] | None = None
self.exit_event = Event() self.exit_event = Event()

View File

@@ -18,9 +18,14 @@ class WindowsDriver(Driver):
"""Powers display and input for Windows.""" """Powers display and input for Windows."""
def __init__( def __init__(
self, console: "Console", target: "MessageTarget", debug: bool = False self,
console: "Console",
target: "MessageTarget",
*,
debug: bool = False,
size: tuple[int, int] | None = None,
) -> None: ) -> None:
super().__init__(console, target, debug) super().__init__(console, target, debug=debug, size=size)
self.in_fileno = sys.stdin.fileno() self.in_fileno = sys.stdin.fileno()
self.out_fileno = sys.stdout.fileno() self.out_fileno = sys.stdout.fileno()

View File

@@ -119,10 +119,14 @@ class Compose(Event, bubble=False, verbose=True):
"""Sent to a widget to request it to compose and mount children.""" """Sent to a widget to request it to compose and mount children."""
class Mount(Event, bubble=False, verbose=True): class Mount(Event, bubble=False, verbose=False):
"""Sent when a widget is *mounted* and may receive messages.""" """Sent when a widget is *mounted* and may receive messages."""
class Unmount(Mount, bubble=False, verbose=False):
"""Sent when a widget is unmounted and may not longer receive messages."""
class Remove(Event, bubble=False): class Remove(Event, bubble=False):
"""Sent to a widget to ask it to remove itself from the DOM.""" """Sent to a widget to ask it to remove itself from the DOM."""

View File

@@ -145,9 +145,7 @@ class GridLayout(Layout):
y2, cell_height = rows[min(max_row, row + row_span)] y2, cell_height = rows[min(max_row, row + row_span)]
cell_size = Size(cell_width + x2 - x, cell_height + y2 - y) cell_size = Size(cell_width + x2 - x, cell_height + y2 - y)
width, height, margin = widget._get_box_model( width, height, margin = widget._get_box_model(
cell_size, cell_size, viewport, fraction_unit, fraction_unit
viewport,
fraction_unit,
) )
region = ( region = (
Region(x, y, int(width + margin.width), int(height + margin.height)) Region(x, y, int(width + margin.width), int(height + margin.height))

View File

@@ -1,12 +1,11 @@
from __future__ import annotations from __future__ import annotations
from fractions import Fraction from fractions import Fraction
from typing import cast
from textual.geometry import Size, Region from .._resolve import resolve_box_models
from textual._layout import ArrangeResult, Layout, WidgetPlacement from ..geometry import Size, Region
from .._layout import ArrangeResult, Layout, WidgetPlacement
from textual.widget import Widget from ..widget import Widget
class HorizontalLayout(Layout): class HorizontalLayout(Layout):
@@ -22,20 +21,16 @@ class HorizontalLayout(Layout):
placements: list[WidgetPlacement] = [] placements: list[WidgetPlacement] = []
add_placement = placements.append add_placement = placements.append
x = max_height = Fraction(0) x = max_height = Fraction(0)
parent_size = parent.outer_size parent_size = parent.outer_size
styles = [child.styles for child in children if child.styles.width is not None] box_models = resolve_box_models(
total_fraction = sum( [child.styles.width for child in children],
[int(style.width.value) for style in styles if style.width.is_fraction] children,
size,
parent_size,
dimension="width",
) )
fraction_unit = Fraction(size.width, total_fraction or 1)
box_models = [
widget._get_box_model(size, parent_size, fraction_unit)
for widget in cast("list[Widget]", children)
]
margins = [ margins = [
max((box1.margin.right, box2.margin.left)) max((box1.margin.right, box2.margin.left))

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from fractions import Fraction from fractions import Fraction
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .._resolve import resolve_box_models
from ..geometry import Region, Size from ..geometry import Region, Size
from .._layout import ArrangeResult, Layout, WidgetPlacement from .._layout import ArrangeResult, Layout, WidgetPlacement
@@ -21,19 +22,15 @@ class VerticalLayout(Layout):
placements: list[WidgetPlacement] = [] placements: list[WidgetPlacement] = []
add_placement = placements.append add_placement = placements.append
parent_size = parent.outer_size parent_size = parent.outer_size
styles = [child.styles for child in children if child.styles.height is not None] box_models = resolve_box_models(
total_fraction = sum( [child.styles.height for child in children],
[int(style.height.value) for style in styles if style.height.is_fraction] children,
size,
parent_size,
dimension="height",
) )
fraction_unit = Fraction(size.height, total_fraction or 1)
box_models = [
widget._get_box_model(size, parent_size, fraction_unit)
for widget in children
]
margins = [ margins = [
max((box1.margin.bottom, box2.margin.top)) max((box1.margin.bottom, box2.margin.top))

View File

@@ -155,7 +155,9 @@ class MessagePump(metaclass=MessagePumpMeta):
return self._pending_message return self._pending_message
finally: finally:
self._pending_message = None self._pending_message = None
message = await self._message_queue.get() message = await self._message_queue.get()
if message is None: if message is None:
self._closed = True self._closed = True
raise MessagePumpClosed("The message pump is now closed") raise MessagePumpClosed("The message pump is now closed")
@@ -266,8 +268,11 @@ class MessagePump(metaclass=MessagePumpMeta):
self.app.screen._invoke_later(message.callback) self.app.screen._invoke_later(message.callback)
def _close_messages_no_wait(self) -> None: def _close_messages_no_wait(self) -> None:
"""Request the message queue to exit.""" """Request the message queue to immediately exit."""
self._message_queue.put_nowait(None) self._message_queue.put_nowait(messages.CloseMessages(sender=self))
async def _on_close_messages(self, message: messages.CloseMessages) -> None:
await self._close_messages()
async def _close_messages(self) -> None: async def _close_messages(self) -> None:
"""Close message queue, and optionally wait for queue to finish processing.""" """Close message queue, and optionally wait for queue to finish processing."""
@@ -278,6 +283,7 @@ class MessagePump(metaclass=MessagePumpMeta):
for timer in stop_timers: for timer in stop_timers:
await timer.stop() await timer.stop()
self._timers.clear() self._timers.clear()
await self._message_queue.put(events.Unmount(sender=self))
await self._message_queue.put(None) await self._message_queue.put(None)
if self._task is not None and asyncio.current_task() != self._task: if self._task is not None and asyncio.current_task() != self._task:
# Ensure everything is closed before returning # Ensure everything is closed before returning
@@ -285,7 +291,8 @@ class MessagePump(metaclass=MessagePumpMeta):
def _start_messages(self) -> None: def _start_messages(self) -> None:
"""Start messages task.""" """Start messages task."""
self._task = asyncio.create_task(self._process_messages()) if self.app._running:
self._task = asyncio.create_task(self._process_messages())
async def _process_messages(self) -> None: async def _process_messages(self) -> None:
self._running = True self._running = True
@@ -370,8 +377,6 @@ class MessagePump(metaclass=MessagePumpMeta):
self.app._handle_exception(error) self.app._handle_exception(error)
break break
log("CLOSED", self)
async def _dispatch_message(self, message: Message) -> None: async def _dispatch_message(self, message: Message) -> None:
"""Dispatch a message received from the message queue. """Dispatch a message received from the message queue.
@@ -424,6 +429,7 @@ class MessagePump(metaclass=MessagePumpMeta):
handler_name = message._handler_name handler_name = message._handler_name
# Look through the MRO to find a handler # Look through the MRO to find a handler
dispatched = False
for cls, method in self._get_dispatch_methods(handler_name, message): for cls, method in self._get_dispatch_methods(handler_name, message):
log.event.verbosity(message.verbose)( log.event.verbosity(message.verbose)(
message, message,
@@ -431,7 +437,10 @@ class MessagePump(metaclass=MessagePumpMeta):
self, self,
f"method=<{cls.__name__}.{handler_name}>", f"method=<{cls.__name__}.{handler_name}>",
) )
dispatched = True
await invoke(method, message) await invoke(method, message)
if not dispatched:
log.event.verbosity(message.verbose)(message, ">>>", self, "method=None")
# Bubble messages up the DOM (if enabled on the message) # Bubble messages up the DOM (if enabled on the message)
if message.bubble and self._parent and not message._stop_propagation: if message.bubble and self._parent and not message._stop_propagation:

View File

@@ -13,6 +13,16 @@ if TYPE_CHECKING:
from .widget import Widget from .widget import Widget
@rich.repr.auto
class CloseMessages(Message, verbose=True):
"""Requests message pump to close."""
@rich.repr.auto
class ExitApp(Message, verbose=True):
"""Exit the app."""
@rich.repr.auto @rich.repr.auto
class Update(Message, verbose=True): class Update(Message, verbose=True):
def __init__(self, sender: MessagePump, widget: Widget): def __init__(self, sender: MessagePump, widget: Widget):

55
src/textual/pilot.py Normal file
View File

@@ -0,0 +1,55 @@
from __future__ import annotations
import rich.repr
import asyncio
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .app import App
@rich.repr.auto(angular=True)
class Pilot:
"""Pilot object to drive an app."""
def __init__(self, app: App) -> None:
self._app = app
def __rich_repr__(self) -> rich.repr.Result:
yield "app", self._app
@property
def app(self) -> App:
"""Get a reference to the application.
Returns:
App: The App instance.
"""
return self._app
async def press(self, *keys: str) -> None:
"""Simulate key-presses.
Args:
*key: Keys to press.
"""
if keys:
await self._app._press_keys(keys)
async def pause(self, delay: float = 50 / 1000) -> None:
"""Insert a pause.
Args:
delay (float, optional): Seconds to pause. Defaults to 50ms.
"""
await asyncio.sleep(delay)
async def exit(self, result: object) -> None:
"""Exit the app with the given result.
Args:
result (object): The app result returned by `run` or `run_async`.
"""
self.app.exit(result)

View File

@@ -5,7 +5,6 @@ from inspect import isawaitable
from typing import TYPE_CHECKING, Any, Callable, Generic, Type, TypeVar, Union from typing import TYPE_CHECKING, Any, Callable, Generic, Type, TypeVar, Union
from weakref import WeakSet from weakref import WeakSet
from . import events from . import events
from ._callback import count_parameters, invoke from ._callback import count_parameters, invoke
from ._types import MessageTarget from ._types import MessageTarget
@@ -16,7 +15,6 @@ if TYPE_CHECKING:
Reactable = Union[Widget, App] Reactable = Union[Widget, App]
ReactiveType = TypeVar("ReactiveType") ReactiveType = TypeVar("ReactiveType")
@@ -37,7 +35,7 @@ class Reactive(Generic[ReactiveType]):
layout (bool, optional): Perform a layout on change. Defaults to False. layout (bool, optional): Perform a layout on change. Defaults to False.
repaint (bool, optional): Perform a repaint on change. Defaults to True. repaint (bool, optional): Perform a repaint on change. Defaults to True.
init (bool, optional): Call watchers on initialize (post mount). Defaults to False. init (bool, optional): Call watchers on initialize (post mount). Defaults to False.
always_update(bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
""" """
def __init__( def __init__(
@@ -47,11 +45,13 @@ class Reactive(Generic[ReactiveType]):
layout: bool = False, layout: bool = False,
repaint: bool = True, repaint: bool = True,
init: bool = False, init: bool = False,
always_update: bool = False,
) -> None: ) -> None:
self._default = default self._default = default
self._layout = layout self._layout = layout
self._repaint = repaint self._repaint = repaint
self._init = init self._init = init
self._always_update = always_update
@classmethod @classmethod
def init( def init(
@@ -60,6 +60,7 @@ class Reactive(Generic[ReactiveType]):
*, *,
layout: bool = False, layout: bool = False,
repaint: bool = True, repaint: bool = True,
always_update: bool = False,
) -> Reactive: ) -> Reactive:
"""A reactive variable that calls watchers and compute on initialize (post mount). """A reactive variable that calls watchers and compute on initialize (post mount).
@@ -67,11 +68,17 @@ class Reactive(Generic[ReactiveType]):
default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default. default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
layout (bool, optional): Perform a layout on change. Defaults to False. layout (bool, optional): Perform a layout on change. Defaults to False.
repaint (bool, optional): Perform a repaint on change. Defaults to True. repaint (bool, optional): Perform a repaint on change. Defaults to True.
always_update(bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
Returns: Returns:
Reactive: A Reactive instance which calls watchers or initialize. Reactive: A Reactive instance which calls watchers or initialize.
""" """
return cls(default, layout=layout, repaint=repaint, init=True) return cls(
default,
layout=layout,
repaint=repaint,
init=True,
always_update=always_update,
)
@classmethod @classmethod
def var( def var(
@@ -153,7 +160,7 @@ class Reactive(Generic[ReactiveType]):
if callable(validate_function) and not first_set: if callable(validate_function) and not first_set:
value = validate_function(value) value = validate_function(value)
# If the value has changed, or this is the first time setting the value # If the value has changed, or this is the first time setting the value
if current_value != value or first_set: if current_value != value or first_set or self._always_update:
# Set the first set flag to False # Set the first set flag to False
setattr(obj, f"__first_set_{self.internal_name}", False) setattr(obj, f"__first_set_{self.internal_name}", False)
# Store the internal value # Store the internal value
@@ -259,7 +266,7 @@ class reactive(Reactive[ReactiveType]):
layout (bool, optional): Perform a layout on change. Defaults to False. layout (bool, optional): Perform a layout on change. Defaults to False.
repaint (bool, optional): Perform a repaint on change. Defaults to True. repaint (bool, optional): Perform a repaint on change. Defaults to True.
init (bool, optional): Call watchers on initialize (post mount). Defaults to True. init (bool, optional): Call watchers on initialize (post mount). Defaults to True.
always_update(bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
""" """
def __init__( def __init__(
@@ -269,8 +276,15 @@ class reactive(Reactive[ReactiveType]):
layout: bool = False, layout: bool = False,
repaint: bool = True, repaint: bool = True,
init: bool = True, init: bool = True,
always_update: bool = False,
) -> None: ) -> None:
super().__init__(default, layout=layout, repaint=repaint, init=init) super().__init__(
default,
layout=layout,
repaint=repaint,
init=init,
always_update=always_update,
)
class var(Reactive[ReactiveType]): class var(Reactive[ReactiveType]):

View File

@@ -12,7 +12,6 @@ from ._callback import invoke
from ._compositor import Compositor, MapGeometry from ._compositor import Compositor, MapGeometry
from .timer import Timer from .timer import Timer
from ._types import CallbackType from ._types import CallbackType
from .dom import DOMNode
from .geometry import Offset, Region, Size from .geometry import Offset, Region, Size
from .reactive import Reactive from .reactive import Reactive
from .renderables.blank import Blank from .renderables.blank import Blank
@@ -61,7 +60,12 @@ class Screen(Widget):
@property @property
def is_current(self) -> bool: def is_current(self) -> bool:
"""Check if this screen is current (i.e. visible to user).""" """Check if this screen is current (i.e. visible to user)."""
return self.app.screen is self from .app import ScreenStackError
try:
return self.app.screen is self
except ScreenStackError:
return False
@property @property
def update_timer(self) -> Timer: def update_timer(self) -> Timer:

View File

@@ -82,11 +82,13 @@ class AwaitMount:
def __await__(self) -> Generator[None, None, None]: def __await__(self) -> Generator[None, None, None]:
async def await_mount() -> None: async def await_mount() -> None:
aws = [ if self._widgets:
create_task(widget._mounted_event.wait()) for widget in self._widgets aws = [
] create_task(widget._mounted_event.wait())
if aws: for widget in self._widgets
await wait(aws) ]
if aws:
await wait(aws)
return await_mount().__await__() return await_mount().__await__()
@@ -359,6 +361,20 @@ class Widget(DOMNode):
"""Clear arrangement cache, forcing a new arrange operation.""" """Clear arrangement cache, forcing a new arrange operation."""
self._arrangement = None self._arrangement = None
def _get_virtual_dom(self) -> Iterable[Widget]:
"""Get widgets not part of the DOM.
Returns:
Iterable[Widget]: An iterable of Widgets.
"""
if self._horizontal_scrollbar is not None:
yield self._horizontal_scrollbar
if self._vertical_scrollbar is not None:
yield self._vertical_scrollbar
if self._scrollbar_corner is not None:
yield self._scrollbar_corner
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> AwaitMount: def mount(self, *anon_widgets: Widget, **widgets: Widget) -> AwaitMount:
"""Mount child widgets (making this widget a container). """Mount child widgets (making this widget a container).
@@ -409,14 +425,19 @@ class Widget(DOMNode):
) )
def _get_box_model( def _get_box_model(
self, container: Size, viewport: Size, fraction_unit: Fraction self,
container: Size,
viewport: Size,
width_fraction: Fraction,
height_fraction: Fraction,
) -> BoxModel: ) -> BoxModel:
"""Process the box model for this widget. """Process the box model for this widget.
Args: Args:
container (Size): The size of the container widget (with a layout) container (Size): The size of the container widget (with a layout)
viewport (Size): The viewport size. viewport (Size): The viewport size.
fraction_unit (Fraction): The unit used for `fr` units. width_fraction (Fraction): A fraction used for 1 `fr` unit on the width dimension.
height_fraction (Fraction):A fraction used for 1 `fr` unit on the height dimension.
Returns: Returns:
BoxModel: The size and margin for this widget. BoxModel: The size and margin for this widget.
@@ -425,7 +446,8 @@ class Widget(DOMNode):
self.styles, self.styles,
container, container,
viewport, viewport,
fraction_unit, width_fraction,
height_fraction,
self.get_content_width, self.get_content_width,
self.get_content_height, self.get_content_height,
) )
@@ -587,6 +609,7 @@ class Widget(DOMNode):
Returns: Returns:
ScrollBar: ScrollBar Widget. ScrollBar: ScrollBar Widget.
""" """
from .scrollbar import ScrollBar from .scrollbar import ScrollBar
if self._horizontal_scrollbar is not None: if self._horizontal_scrollbar is not None:
@@ -595,13 +618,12 @@ class Widget(DOMNode):
vertical=False, name="horizontal", thickness=self.scrollbar_size_horizontal vertical=False, name="horizontal", thickness=self.scrollbar_size_horizontal
) )
self._horizontal_scrollbar.display = False self._horizontal_scrollbar.display = False
self.app._start_widget(self, scroll_bar) self.app._start_widget(self, scroll_bar)
return scroll_bar return scroll_bar
def _refresh_scrollbars(self) -> None: def _refresh_scrollbars(self) -> None:
"""Refresh scrollbar visibility.""" """Refresh scrollbar visibility."""
if not self.is_scrollable: if not self.is_scrollable or not self.container_size:
return return
styles = self.styles styles = self.styles
@@ -1930,8 +1952,13 @@ class Widget(DOMNode):
async def handle_key(self, event: events.Key) -> bool: async def handle_key(self, event: events.Key) -> bool:
return await self.dispatch_key(event) return await self.dispatch_key(event)
async def _on_compose(self, event: events.Compose) -> None: async def _on_compose(self) -> None:
widgets = list(self.compose()) try:
widgets = list(self.compose())
except TypeError as error:
raise TypeError(
f"{self!r} compose() returned an invalid response; {error}"
) from None
await self.mount(*widgets) await self.mount(*widgets)
def _on_mount(self, event: events.Mount) -> None: def _on_mount(self, event: events.Mount) -> None:

View File

@@ -18,8 +18,6 @@ from textual.css.styles import Styles, RenderStyles
from textual.dom import DOMNode from textual.dom import DOMNode
from textual.widget import Widget from textual.widget import Widget
from tests.utilities.test_app import AppTest
def test_styles_reset(): def test_styles_reset():
styles = Styles() styles = Styles()
@@ -206,88 +204,3 @@ def test_widget_style_size_fails_if_data_type_is_not_supported(size_dimension_in
with pytest.raises(StyleValueError): with pytest.raises(StyleValueError):
widget.styles.width = size_dimension_input widget.styles.width = size_dimension_input
@pytest.mark.asyncio
@pytest.mark.parametrize(
"overflow_y,scrollbar_gutter,scrollbar_size,text_length,expected_text_widget_width,expects_vertical_scrollbar",
(
# ------------------------------------------------
# ----- Let's start with `overflow-y: auto`:
# short text: full width, no scrollbar
["auto", "auto", 1, "short_text", 80, False],
# long text: reduced width, scrollbar
["auto", "auto", 1, "long_text", 78, True],
# short text, `scrollbar-gutter: stable`: reduced width, no scrollbar
["auto", "stable", 1, "short_text", 78, False],
# long text, `scrollbar-gutter: stable`: reduced width, scrollbar
["auto", "stable", 1, "long_text", 78, True],
# ------------------------------------------------
# ----- And now let's see the behaviour with `overflow-y: scroll`:
# short text: reduced width, scrollbar
["scroll", "auto", 1, "short_text", 78, True],
# long text: reduced width, scrollbar
["scroll", "auto", 1, "long_text", 78, True],
# short text, `scrollbar-gutter: stable`: reduced width, scrollbar
["scroll", "stable", 1, "short_text", 78, True],
# long text, `scrollbar-gutter: stable`: reduced width, scrollbar
["scroll", "stable", 1, "long_text", 78, True],
# ------------------------------------------------
# ----- Finally, let's check the behaviour with `overflow-y: hidden`:
# short text: full width, no scrollbar
["hidden", "auto", 1, "short_text", 80, False],
# long text: full width, no scrollbar
["hidden", "auto", 1, "long_text", 80, False],
# short text, `scrollbar-gutter: stable`: reduced width, no scrollbar
["hidden", "stable", 1, "short_text", 78, False],
# long text, `scrollbar-gutter: stable`: reduced width, no scrollbar
["hidden", "stable", 1, "long_text", 78, False],
# ------------------------------------------------
# ----- Bonus round with a custom scrollbar size, now that we can set this:
["auto", "auto", 3, "short_text", 80, False],
["auto", "auto", 3, "long_text", 77, True],
["scroll", "auto", 3, "short_text", 77, True],
["scroll", "stable", 3, "short_text", 77, True],
["hidden", "auto", 3, "long_text", 80, False],
["hidden", "stable", 3, "short_text", 77, False],
),
)
async def test_scrollbar_gutter(
overflow_y: str,
scrollbar_gutter: str,
scrollbar_size: int,
text_length: Literal["short_text", "long_text"],
expected_text_widget_width: int,
expects_vertical_scrollbar: bool,
):
from rich.text import Text
from textual.geometry import Size
class TextWidget(Widget):
def render(self) -> Text:
text_multiplier = 10 if text_length == "long_text" else 2
return Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit liber a a a."
* text_multiplier
)
container = Widget()
container.styles.height = 3
container.styles.overflow_y = overflow_y
container.styles.scrollbar_gutter = scrollbar_gutter
if scrollbar_size > 1:
container.styles.scrollbar_size_vertical = scrollbar_size
text_widget = TextWidget()
text_widget.styles.height = "auto"
container._add_child(text_widget)
class MyTestApp(AppTest):
def compose(self) -> ComposeResult:
yield container
app = MyTestApp(test_name="scrollbar_gutter", size=Size(80, 10))
await app.boot_and_shutdown()
assert text_widget.outer_size.width == expected_text_widget_width
assert container.scrollbars_enabled[0] is expects_vertical_scrollbar

File diff suppressed because one or more lines are too long

View File

@@ -41,7 +41,7 @@ def snap_compare(
def compare( def compare(
app_path: str, app_path: str,
press: Iterable[str] = ("_",), press: Iterable[str] = ("_",),
terminal_size: tuple[int, int] = (24, 80), terminal_size: tuple[int, int] = (80, 24),
) -> bool: ) -> bool:
""" """
Compare a current screenshot of the app running at app_path, with Compare a current screenshot of the app running at app_path, with
@@ -52,14 +52,13 @@ def snap_compare(
Args: Args:
app_path (str): The path of the app. app_path (str): The path of the app.
press (Iterable[str]): Key presses to run before taking screenshot. "_" is a short pause. press (Iterable[str]): Key presses to run before taking screenshot. "_" is a short pause.
terminal_size (tuple[int, int]): A pair of integers (rows, columns), representing terminal size. terminal_size (tuple[int, int]): A pair of integers (WIDTH, SIZE), representing terminal size.
Returns: Returns:
bool: True if the screenshot matches the snapshot. bool: True if the screenshot matches the snapshot.
""" """
node = request.node node = request.node
app = import_app(app_path) app = import_app(app_path)
compare.app = app
actual_screenshot = take_svg_screenshot( actual_screenshot = take_svg_screenshot(
app=app, app=app,
press=press, press=press,
@@ -69,7 +68,9 @@ def snap_compare(
if result is False: if result is False:
# The split and join below is a mad hack, sorry... # The split and join below is a mad hack, sorry...
node.stash[TEXTUAL_SNAPSHOT_SVG_KEY] = "\n".join(str(snapshot).splitlines()[1:-1]) node.stash[TEXTUAL_SNAPSHOT_SVG_KEY] = "\n".join(
str(snapshot).splitlines()[1:-1]
)
node.stash[TEXTUAL_ACTUAL_SVG_KEY] = actual_screenshot node.stash[TEXTUAL_ACTUAL_SVG_KEY] = actual_screenshot
node.stash[TEXTUAL_APP_KEY] = app node.stash[TEXTUAL_APP_KEY] = app
else: else:
@@ -85,6 +86,7 @@ class SvgSnapshotDiff:
"""Model representing a diff between current screenshot of an app, """Model representing a diff between current screenshot of an app,
and the snapshot on disk. This is ultimately intended to be used in and the snapshot on disk. This is ultimately intended to be used in
a Jinja2 template.""" a Jinja2 template."""
snapshot: Optional[str] snapshot: Optional[str]
actual: Optional[str] actual: Optional[str]
test_name: str test_name: str
@@ -119,7 +121,7 @@ def pytest_sessionfinish(
snapshot=str(snapshot_svg), snapshot=str(snapshot_svg),
actual=str(actual_svg), actual=str(actual_svg),
file_similarity=100 file_similarity=100
* difflib.SequenceMatcher( * difflib.SequenceMatcher(
a=str(snapshot_svg), b=str(actual_svg) a=str(snapshot_svg), b=str(actual_svg)
).ratio(), ).ratio(),
test_name=name, test_name=name,
@@ -176,7 +178,9 @@ def pytest_terminal_summary(
if diffs: if diffs:
snapshot_report_location = config._textual_snapshot_html_report snapshot_report_location = config._textual_snapshot_html_report
console.rule("[b red]Textual Snapshot Report", style="red") console.rule("[b red]Textual Snapshot Report", style="red")
console.print(f"\n[black on red]{len(diffs)} mismatched snapshots[/]\n" console.print(
f"\n[b]View the [link=file://{snapshot_report_location}]failure report[/].\n") f"\n[black on red]{len(diffs)} mismatched snapshots[/]\n"
f"\n[b]View the [link=file://{snapshot_report_location}]failure report[/].\n"
)
console.print(f"[dim]{snapshot_report_location}\n") console.print(f"[dim]{snapshot_report_location}\n")
console.rule(style="red") console.rule(style="red")

View File

@@ -66,22 +66,11 @@ def test_input_and_focus(snap_compare):
] ]
assert snap_compare("docs/examples/widgets/input.py", press=press) assert snap_compare("docs/examples/widgets/input.py", press=press)
# Assert that the state of the Input is what we'd expect
app: App = snap_compare.app
input: Input = app.query_one(Input)
assert input.value == "Darren"
assert input.cursor_position == 6
assert input.view_position == 0
def test_buttons_render(snap_compare): def test_buttons_render(snap_compare):
# Testing button rendering. We press tab to focus the first button too. # Testing button rendering. We press tab to focus the first button too.
assert snap_compare("docs/examples/widgets/button.py", press=["tab"]) assert snap_compare("docs/examples/widgets/button.py", press=["tab"])
app = snap_compare.app
button: Button = app.query_one(Button)
assert app.focused is button
def test_datatable_render(snap_compare): def test_datatable_render(snap_compare):
press = ["tab", "down", "down", "right", "up", "left"] press = ["tab", "down", "down", "right", "up", "left"]
@@ -100,6 +89,10 @@ def test_textlog_max_lines(snap_compare):
assert snap_compare("tests/snapshots/textlog_max_lines.py", press=list("abcde")) assert snap_compare("tests/snapshots/textlog_max_lines.py", press=list("abcde"))
def test_fr_units(snap_compare):
assert snap_compare("tests/snapshots/fr_units.py")
# --- CSS properties --- # --- CSS properties ---
# We have a canonical example for each CSS property that is shown in their docs. # We have a canonical example for each CSS property that is shown in their docs.
# If any of these change, something has likely broken, so snapshot each of them. # If any of these change, something has likely broken, so snapshot each of them.

View File

@@ -0,0 +1,55 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Static
class StaticText(Static):
pass
class FRApp(App):
CSS = """
StaticText {
height: 1fr;
background: $boost;
border: heavy white;
}
#foo {
width: 10;
}
#bar {
width: 1fr;
}
#baz {
width: 8;
}
#header {
height: 1fr
}
Horizontal {
height: 2fr;
}
#footer {
height: 4;
}
"""
def compose(self) -> ComposeResult:
yield Vertical(
StaticText("HEADER", id="header"),
Horizontal(
StaticText("foo", id="foo"),
StaticText("bar", id="bar"),
StaticText("baz", id="baz"),
),
StaticText("FOOTER", id="footer"),
)
if __name__ == "__main__":
app = FRApp()
app.run()

23
tests/test_auto_pilot.py Normal file
View File

@@ -0,0 +1,23 @@
from textual.app import App
from textual.pilot import Pilot
from textual import events
def test_auto_pilot() -> None:
keys_pressed: list[str] = []
class TestApp(App):
def on_key(self, event: events.Key) -> None:
keys_pressed.append(event.key)
async def auto_pilot(pilot: Pilot) -> None:
await pilot.press("tab", *"foo")
await pilot.pause(1 / 100)
await pilot.exit("bar")
app = TestApp()
result = app.run(headless=True, auto_pilot=auto_pilot)
assert result == "bar"
assert keys_pressed == ["tab", "f", "o", "o"]

View File

@@ -1,6 +1,8 @@
import asyncio
from time import time from time import time
from textual.app import App from textual.app import App
from textual.pilot import Pilot
class RefreshApp(App[float]): class RefreshApp(App[float]):
@@ -22,7 +24,10 @@ class RefreshApp(App[float]):
def test_auto_refresh(): def test_auto_refresh():
app = RefreshApp() app = RefreshApp()
elapsed = app.run(quit_after=1, headless=True) async def quit_after(pilot: Pilot) -> None:
await asyncio.sleep(1)
elapsed = app.run(auto_pilot=quit_after, headless=True)
assert elapsed is not None assert elapsed is not None
# CI can run slower, so we need to give this a bit of margin # CI can run slower, so we need to give this a bit of margin
assert 0.2 <= elapsed < 0.8 assert 0.2 <= elapsed < 0.8

View File

@@ -26,7 +26,7 @@ def test_content_box():
assert False, "must not be called" assert False, "must not be called"
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
# Size should be inclusive of padding / border # Size should be inclusive of padding / border
assert box_model == BoxModel(Fraction(10), Fraction(8), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(10), Fraction(8), Spacing(0, 0, 0, 0))
@@ -35,7 +35,7 @@ def test_content_box():
styles.box_sizing = "content-box" styles.box_sizing = "content-box"
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
# width and height have added padding / border to accommodate content # width and height have added padding / border to accommodate content
assert box_model == BoxModel(Fraction(14), Fraction(12), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(14), Fraction(12), Spacing(0, 0, 0, 0))
@@ -53,7 +53,7 @@ def test_width():
return 10 return 10
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(60), Fraction(20), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(60), Fraction(20), Spacing(0, 0, 0, 0))
@@ -61,7 +61,7 @@ def test_width():
styles.margin = (1, 2, 3, 4) styles.margin = (1, 2, 3, 4)
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
@@ -69,7 +69,7 @@ def test_width():
styles.width = "auto" styles.width = "auto"
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
# Setting width to auto should call get_auto_width # Setting width to auto should call get_auto_width
assert box_model == BoxModel(Fraction(10), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(10), Fraction(16), Spacing(1, 2, 3, 4))
@@ -78,7 +78,7 @@ def test_width():
styles.width = "100vw" styles.width = "100vw"
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(80), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(80), Fraction(16), Spacing(1, 2, 3, 4))
@@ -86,7 +86,7 @@ def test_width():
styles.width = "100%" styles.width = "100%"
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
@@ -94,7 +94,7 @@ def test_width():
styles.max_width = "50%" styles.max_width = "50%"
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(30), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(30), Fraction(16), Spacing(1, 2, 3, 4))
@@ -111,7 +111,7 @@ def test_height():
return 10 return 10
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(60), Fraction(20), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(60), Fraction(20), Spacing(0, 0, 0, 0))
@@ -119,7 +119,7 @@ def test_height():
styles.margin = (1, 2, 3, 4) styles.margin = (1, 2, 3, 4)
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
@@ -127,7 +127,7 @@ def test_height():
styles.height = "100vh" styles.height = "100vh"
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(54), Fraction(24), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(24), Spacing(1, 2, 3, 4))
@@ -135,7 +135,7 @@ def test_height():
styles.height = "100%" styles.height = "100%"
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
@@ -143,7 +143,7 @@ def test_height():
styles.margin = 2 styles.margin = 2
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(56), Fraction(10), Spacing(2, 2, 2, 2)) assert box_model == BoxModel(Fraction(56), Fraction(10), Spacing(2, 2, 2, 2))
@@ -152,7 +152,7 @@ def test_height():
styles.max_height = "50%" styles.max_height = "50%"
box_model = get_box_model( box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(54), Fraction(10), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(10), Spacing(1, 2, 3, 4))
@@ -173,7 +173,7 @@ def test_max():
assert False, "must not be called" assert False, "must not be called"
box_model = get_box_model( box_model = get_box_model(
styles, Size(40, 30), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(40, 30), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0))
@@ -194,6 +194,6 @@ def test_min():
assert False, "must not be called" assert False, "must not be called"
box_model = get_box_model( box_model = get_box_model(
styles, Size(40, 30), Size(80, 24), one, get_auto_width, get_auto_height styles, Size(40, 30), Size(80, 24), one, one, get_auto_width, get_auto_height
) )
assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0))

View File

@@ -1,116 +0,0 @@
from __future__ import annotations
from typing import Sequence, cast
import pytest
from tests.utilities.test_app import AppTest
from textual.app import ComposeResult
from textual.geometry import Size
from textual.widget import Widget
from textual.widgets import Placeholder
pytestmark = pytest.mark.integration_test
SCREEN_SIZE = Size(100, 30)
@pytest.mark.skip("Needs a rethink")
@pytest.mark.asyncio
@pytest.mark.parametrize(
(
"screen_size",
"placeholders_count",
"scroll_to_placeholder_id",
"scroll_to_animate",
"waiting_duration",
"last_screen_expected_placeholder_ids",
),
(
[SCREEN_SIZE, 10, None, None, 0.01, (0, 1, 2, 3, 4)],
[SCREEN_SIZE, 10, "placeholder_3", False, 0.01, (0, 1, 2, 3, 4)],
[SCREEN_SIZE, 10, "placeholder_5", False, 0.01, (1, 2, 3, 4, 5)],
[SCREEN_SIZE, 10, "placeholder_7", False, 0.01, (3, 4, 5, 6, 7)],
[SCREEN_SIZE, 10, "placeholder_9", False, 0.01, (5, 6, 7, 8, 9)],
# N.B. Scroll duration is hard-coded to 0.2 in the `scroll_to_widget` method atm
# Waiting for this duration should allow us to see the scroll finished:
[SCREEN_SIZE, 10, "placeholder_9", True, 0.21, (5, 6, 7, 8, 9)],
# After having waited for approximately half of the scrolling duration, we should
# see the middle Placeholders as we're scrolling towards the last of them.
[SCREEN_SIZE, 10, "placeholder_9", True, 0.1, (4, 5, 6, 7, 8)],
),
)
async def test_scroll_to_widget(
screen_size: Size,
placeholders_count: int,
scroll_to_animate: bool | None,
scroll_to_placeholder_id: str | None,
waiting_duration: float | None,
last_screen_expected_placeholder_ids: Sequence[int],
):
class VerticalContainer(Widget):
DEFAULT_CSS = """
VerticalContainer {
layout: vertical;
overflow: hidden auto;
}
VerticalContainer Placeholder {
margin: 1 0;
height: 5;
}
"""
class MyTestApp(AppTest):
DEFAULT_CSS = """
Placeholder {
height: 5; /* minimal height to see the name of a Placeholder */
}
"""
def compose(self) -> ComposeResult:
placeholders = [
Placeholder(id=f"placeholder_{i}", name=f"Placeholder #{i}")
for i in range(placeholders_count)
]
yield VerticalContainer(*placeholders, id="root")
app = MyTestApp(size=screen_size, test_name="scroll_to_widget")
async with app.in_running_state(waiting_duration_after_yield=waiting_duration or 0):
if scroll_to_placeholder_id:
target_widget_container = cast(Widget, app.query("#root").first())
target_widget = cast(
Widget, app.query(f"#{scroll_to_placeholder_id}").first()
)
target_widget_container.scroll_to_widget(
target_widget, animate=scroll_to_animate
)
last_display_capture = app.last_display_capture
placeholders_visibility_by_id = {
id_: f"placeholder_{id_}" in last_display_capture
for id_ in range(placeholders_count)
}
print(placeholders_visibility_by_id)
# Let's start by checking placeholders that should be visible:
for placeholder_id in last_screen_expected_placeholder_ids:
assert placeholders_visibility_by_id[placeholder_id] is True, (
f"Placeholder '{placeholder_id}' should be visible but isn't"
f" :: placeholders_visibility_by_id={placeholders_visibility_by_id}"
)
# Ok, now for placeholders that should *not* be visible:
# We're simply going to check that all the placeholders that are not in
# `last_screen_expected_placeholder_ids` are not on the screen:
last_screen_expected_out_of_viewport_placeholder_ids = sorted(
tuple(
set(range(placeholders_count)) - set(last_screen_expected_placeholder_ids)
)
)
for placeholder_id in last_screen_expected_out_of_viewport_placeholder_ids:
assert placeholders_visibility_by_id[placeholder_id] is False, (
f"Placeholder '{placeholder_id}' should not be visible but is"
f" :: placeholders_visibility_by_id={placeholders_visibility_by_id}"
)

View File

@@ -89,4 +89,4 @@ async def test_screens():
screen1.remove() screen1.remove()
screen2.remove() screen2.remove()
screen3.remove() screen3.remove()
await app.shutdown() await app._shutdown()

21
tests/test_test_runner.py Normal file
View File

@@ -0,0 +1,21 @@
from textual.app import App
from textual import events
async def test_run_test() -> None:
"""Test the run_test context manager."""
keys_pressed: list[str] = []
class TestApp(App[str]):
def on_key(self, event: events.Key) -> None:
keys_pressed.append(event.key)
app = TestApp()
async with app.run_test() as pilot:
assert str(pilot) == "<Pilot app=TestApp(title='TestApp')>"
await pilot.press("tab", *"foo")
await pilot.pause(1 / 100)
await pilot.exit("bar")
assert app.return_value == "bar"
assert keys_pressed == ["tab", "f", "o", "o"]

49
tests/test_unmount.py Normal file
View File

@@ -0,0 +1,49 @@
from textual.app import App, ComposeResult
from textual import events
from textual.containers import Container
from textual.screen import Screen
async def test_unmount():
"""Test unmount events are received in reverse DOM order."""
unmount_ids: list[str] = []
class UnmountWidget(Container):
def on_unmount(self, event: events.Unmount):
unmount_ids.append(f"{self.__class__.__name__}#{self.id}")
class MyScreen(Screen):
def compose(self) -> ComposeResult:
yield UnmountWidget(
UnmountWidget(
UnmountWidget(id="bar1"), UnmountWidget(id="bar2"), id="bar"
),
UnmountWidget(
UnmountWidget(id="baz1"), UnmountWidget(id="baz2"), id="baz"
),
id="top",
)
def on_unmount(self, event: events.Unmount):
unmount_ids.append(f"{self.__class__.__name__}#{self.id}")
class UnmountApp(App):
async def on_mount(self) -> None:
await self.push_screen(MyScreen(id="main"))
app = UnmountApp()
async with app.run_test() as pilot:
await pilot.exit(None)
expected = [
"UnmountWidget#bar1",
"UnmountWidget#bar2",
"UnmountWidget#baz1",
"UnmountWidget#baz2",
"UnmountWidget#bar",
"UnmountWidget#baz",
"UnmountWidget#top",
"MyScreen#main",
]
assert unmount_ids == expected

View File

@@ -1,353 +0,0 @@
from __future__ import annotations
import asyncio
import contextlib
import io
from math import ceil
from pathlib import Path
from time import monotonic
from typing import AsyncContextManager, cast, ContextManager
from unittest import mock
from rich.console import Console
from textual import events, errors
from textual._ansi_sequences import SYNC_START
from textual._clock import _Clock
from textual._context import active_app
from textual.app import App, ComposeResult
from textual.app import WINDOWS
from textual.driver import Driver
from textual.geometry import Size, Region
# N.B. These classes would better be named TestApp/TestConsole/TestDriver/etc,
# but it makes pytest emit warning as it will try to collect them as classes containing test cases :-/
class AppTest(App):
def __init__(self, *, test_name: str, size: Size):
# Tests will log in "/tests/test.[test name].log":
log_path = Path(__file__).parent.parent / f"test.{test_name}.log"
super().__init__(
driver_class=DriverTest,
)
# Let's disable all features by default
self.features = frozenset()
# We need this so the "start buffeting"` is always sent for a screen refresh,
# whatever the environment:
# (we use it to slice the output into distinct full screens displays)
self._sync_available = True
self._size = size
self._console = ConsoleTest(width=size.width, height=size.height)
self._error_console = ConsoleTest(width=size.width, height=size.height)
def log_tree(self) -> None:
"""Handy shortcut when testing stuff"""
self.log(self.tree)
def compose(self) -> ComposeResult:
raise NotImplementedError(
"Create a subclass of TestApp and override its `compose()` method, rather than using TestApp directly"
)
def in_running_state(
self,
*,
time_mocking_ticks_granularity_fps: int = 60, # i.e. when moving forward by 1 second we'll do it though 60 ticks
waiting_duration_after_initialisation: float = 1,
waiting_duration_after_yield: float = 0,
) -> AsyncContextManager[ClockMock]:
async def run_app() -> None:
await self._process_messages()
@contextlib.asynccontextmanager
async def get_running_state_context_manager():
with mock_textual_timers(
ticks_granularity_fps=time_mocking_ticks_granularity_fps
) as clock_mock:
run_task = asyncio.create_task(run_app())
# We have to do this because `run_app()` is running in its own async task, and our test is going to
# run in this one - so the app must also be the active App in our current context:
self._set_active()
await clock_mock.advance_clock(waiting_duration_after_initialisation)
# make sure the App has entered its main loop at this stage:
assert self._driver is not None
await self.force_full_screen_update()
# And now it's time to pass the torch on to the test function!
# We provide the `move_clock_forward` function to it,
# so it can also do some time-based Textual stuff if it needs to:
yield clock_mock
await clock_mock.advance_clock(waiting_duration_after_yield)
# Make sure our screen is up-to-date before exiting the context manager,
# so tests using our `last_display_capture` for example can assert things on a fully refreshed screen:
await self.force_full_screen_update()
# End of simulated time: we just shut down ourselves:
assert not run_task.done()
await self.shutdown()
await run_task
return get_running_state_context_manager()
async def boot_and_shutdown(
self,
*,
waiting_duration_after_initialisation: float = 0.001,
waiting_duration_before_shutdown: float = 0,
):
"""Just a commodity shortcut for `async with app.in_running_state(): pass`, for simple cases"""
async with self.in_running_state(
waiting_duration_after_initialisation=waiting_duration_after_initialisation,
waiting_duration_after_yield=waiting_duration_before_shutdown,
):
pass
def get_char_at(self, x: int, y: int) -> str:
"""Get the character at the given cell or empty string
Args:
x (int): X position within the Layout
y (int): Y position within the Layout
Returns:
str: The character at the cell (x, y) within the Layout
"""
# N.B. Basically a copy-paste-and-slightly-adapt of `Compositor.get_style_at()`
try:
widget, region = self.get_widget_at(x, y)
except errors.NoWidget:
return ""
if widget not in self.screen._compositor.visible_widgets:
return ""
x -= region.x
y -= region.y
lines = widget.render_lines(Region(0, y, region.width, 1))
if not lines:
return ""
end = 0
for segment in lines[0]:
end += segment.cell_length
if x < end:
return segment.text[0]
return ""
async def force_full_screen_update(
self, *, repaint: bool = True, layout: bool = True
) -> None:
try:
screen = self.screen
except IndexError:
return # the app may not have a screen yet
# We artificially tell the Compositor that the whole area should be refreshed
screen._compositor._dirty_regions = {
Region(0, 0, screen.outer_size.width, screen.outer_size.height),
}
screen.refresh(repaint=repaint, layout=layout)
# We also have to make sure we have at least one dirty widget, or `screen._on_update()` will early return:
screen._dirty_widgets.add(screen)
screen._on_timer_update()
await let_asyncio_process_some_events()
def _handle_exception(self, error: Exception) -> None:
# In tests we want the errors to be raised, rather than printed to a Console
raise error
def run(self):
raise NotImplementedError(
"Use `async with my_test_app.in_running_state()` rather than `my_test_app.run()`"
)
@property
def active_app(self) -> App | None:
return active_app.get()
@property
def total_capture(self) -> str | None:
return self.console.file.getvalue()
@property
def last_display_capture(self) -> str | None:
total_capture = self.total_capture
if not total_capture:
return None
screen_captures = total_capture.split(SYNC_START)
for single_screen_capture in reversed(screen_captures):
if len(single_screen_capture) > 30:
# let's return the last occurrence of a screen that seem to be properly "fully-paint"
return single_screen_capture
return None
@property
def console(self) -> ConsoleTest:
return self._console
@console.setter
def console(self, console: Console) -> None:
"""This is a no-op, the console is always a TestConsole"""
return
@property
def error_console(self) -> ConsoleTest:
return self._error_console
@error_console.setter
def error_console(self, console: Console) -> None:
"""This is a no-op, the error console is always a TestConsole"""
return
class ConsoleTest(Console):
def __init__(self, *, width: int, height: int):
file = io.StringIO()
super().__init__(
color_system="256",
file=file,
width=width,
height=height,
force_terminal=False,
legacy_windows=False,
)
@property
def file(self) -> io.StringIO:
return cast(io.StringIO, self._file)
@property
def is_dumb_terminal(self) -> bool:
return False
class DriverTest(Driver):
def start_application_mode(self) -> None:
size = Size(self.console.size.width, self.console.size.height)
event = events.Resize(self._target, size, size)
asyncio.run_coroutine_threadsafe(
self._target.post_message(event),
loop=asyncio.get_running_loop(),
)
def disable_input(self) -> None:
pass
def stop_application_mode(self) -> None:
pass
# It seems that we have to give _way more_ time to `asyncio` on Windows in order to see our different awaiters
# properly triggered when we pause our own "move clock forward" loop.
# It could be caused by the fact that the time resolution for `asyncio` on this platform seems rather low:
# > The resolution of the monotonic clock on Windows is usually around 15.6 msec.
# > The best resolution is 0.5 msec.
# @link https://docs.python.org/3/library/asyncio-platforms.html:
ASYNCIO_EVENTS_PROCESSING_REQUIRED_PERIOD = 0.025 if WINDOWS else 0.005
async def let_asyncio_process_some_events() -> None:
await asyncio.sleep(ASYNCIO_EVENTS_PROCESSING_REQUIRED_PERIOD)
class ClockMock(_Clock):
# To avoid issues with floats we will store the current time as an integer internally.
# Tenths of microseconds should be a good enough granularity:
TIME_RESOLUTION = 10_000_000
def __init__(
self,
*,
ticks_granularity_fps: int = 60,
):
self._ticks_granularity_fps = ticks_granularity_fps
self._single_tick_duration = int(self.TIME_RESOLUTION / ticks_granularity_fps)
self._start_time: int = -1
self._current_time: int = -1
# For each call to our `sleep` method we will store an asyncio.Event
# and the time at which we should trigger it:
self._pending_sleep_events: dict[int, list[asyncio.Event]] = {}
def get_time_no_wait(self) -> float:
if self._current_time == -1:
self._start_clock()
return self._current_time / self.TIME_RESOLUTION
async def sleep(self, seconds: float) -> None:
event = asyncio.Event()
internal_waiting_duration = int(seconds * self.TIME_RESOLUTION)
target_event_monotonic_time = self._current_time + internal_waiting_duration
self._pending_sleep_events.setdefault(target_event_monotonic_time, []).append(
event
)
# Ok, let's wait for this Event
# (which can only be "unlocked" by calls to `advance_clock()`)
await event.wait()
async def advance_clock(self, seconds: float) -> None:
"""
Artificially advance the Textual clock forward.
Args:
seconds: for each second we will artificially tick `ticks_granularity_fps` times
"""
if self._current_time == -1:
self._start_clock()
ticks_count = ceil(seconds * self._ticks_granularity_fps)
activated_timers_count_total = 0 # useful when debugging this code :-)
for tick_counter in range(ticks_count):
self._current_time += self._single_tick_duration
activated_timers_count = self._check_sleep_timers_to_activate()
activated_timers_count_total += activated_timers_count
# Now that we likely unlocked some occurrences of `await sleep(duration)`,
# let's give an opportunity to asyncio-related stuff to happen:
if activated_timers_count:
await let_asyncio_process_some_events()
await let_asyncio_process_some_events()
def _start_clock(self) -> None:
# N.B. `start_time` is not actually used, but it is useful to have when we set breakpoints there :-)
self._start_time = self._current_time = int(monotonic() * self.TIME_RESOLUTION)
def _check_sleep_timers_to_activate(self) -> int:
activated_timers_count = 0
activated_events_times_to_clear: list[int] = []
for (monotonic_time, target_events) in self._pending_sleep_events.items():
if self._current_time < monotonic_time:
continue # not time for you yet, dear awaiter...
# Right, let's release these waiting events!
for event in target_events:
event.set()
activated_timers_count += len(target_events)
# ...and let's mark it for removal:
activated_events_times_to_clear.append(monotonic_time)
for event_time_to_clear in activated_events_times_to_clear:
del self._pending_sleep_events[event_time_to_clear]
return activated_timers_count
def mock_textual_timers(
*,
ticks_granularity_fps: int = 60,
) -> ContextManager[ClockMock]:
@contextlib.contextmanager
def mock_textual_timers_context_manager():
clock_mock = ClockMock(ticks_granularity_fps=ticks_granularity_fps)
with mock.patch("textual._clock._clock", new=clock_mock):
yield clock_mock
return mock_textual_timers_context_manager()