diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index ee439baf0..ad5e23f6f 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
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:
run:
shell: bash
@@ -20,9 +20,9 @@ jobs:
python-version: ${{ matrix.python-version }}
architecture: x64
- name: Install and configure Poetry
- uses: snok/install-poetry@v1.1.6
+ uses: snok/install-poetry@v1.3.3
with:
- version: 1.1.6
+ version: 1.2.2
virtualenvs-in-project: true
- name: Install dependencies
run: poetry install --extras "dev"
diff --git a/.gitignore b/.gitignore
index 4580aac9a..8e8a83d68 100644
--- a/.gitignore
+++ b/.gitignore
@@ -116,3 +116,6 @@ venv.bak/
# Snapshot testing report output directory
tests/snapshot_tests/output
+
+# Sandbox folder - convenient place for us to develop small test apps without leaving the repo
+sandbox/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad4f09490..69b41ae86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,24 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.2.2] - Unreleased
+### Fixed
+
+- Fixed issue where scrollbars weren't being unmounted
+
### Changed
- 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
diff --git a/docs/examples/tutorial/stopwatch03.css b/docs/examples/tutorial/stopwatch03.css
index b911dfeae..44df216e1 100644
--- a/docs/examples/tutorial/stopwatch03.css
+++ b/docs/examples/tutorial/stopwatch03.css
@@ -2,8 +2,9 @@ Stopwatch {
layout: horizontal;
background: $boost;
height: 5;
- padding: 1;
margin: 1;
+ min-width: 50;
+ padding: 1;
}
TimeDisplay {
diff --git a/docs/examples/tutorial/stopwatch04.css b/docs/examples/tutorial/stopwatch04.css
index 2bc514b00..27022ecfb 100644
--- a/docs/examples/tutorial/stopwatch04.css
+++ b/docs/examples/tutorial/stopwatch04.css
@@ -2,8 +2,8 @@ Stopwatch {
layout: horizontal;
background: $boost;
height: 5;
- min-width: 50;
margin: 1;
+ min-width: 50;
padding: 1;
}
diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md
index 59e9a805f..fa0d1e7e9 100644
--- a/docs/guide/devtools.md
+++ b/docs/guide/devtools.md
@@ -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
```
-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.
diff --git a/docs/guide/events.md b/docs/guide/events.md
index 51821aeca..2fa5195c2 100644
--- a/docs/guide/events.md
+++ b/docs/guide/events.md
@@ -4,7 +4,7 @@ We've used event handler methods in many of the examples in this guide. This cha
## 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.
@@ -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.
-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.
@@ -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"
-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
@@ -75,7 +75,7 @@ As before, the event bubbles to its parent (the App class).
--8<-- "docs/images/events/bubble3.excalidraw.svg"
-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
@@ -110,7 +110,7 @@ The message class is defined within the widget class itself. This is not strictl
## 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.
@@ -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.
- 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.
@@ -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.
-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
diff --git a/docs/guide/reactivity.md b/docs/guide/reactivity.md
index b4c0bdebc..44735c5aa 100644
--- a/docs/guide/reactivity.md
+++ b/docs/guide/reactivity.md
@@ -165,7 +165,11 @@ If you click the buttons in the above example it will show the current count. Wh
## 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"`.
@@ -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.
+### 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 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.
diff --git a/docs/reference/pilot.md b/docs/reference/pilot.md
new file mode 100644
index 000000000..e1db65812
--- /dev/null
+++ b/docs/reference/pilot.md
@@ -0,0 +1 @@
+::: textual.pilot
diff --git a/docs/tutorial.md b/docs/tutorial.md
index 7fa4486cd..b4f44b60c 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -108,7 +108,7 @@ Let's examine `stopwatch01.py` in more detail.
--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:
@@ -165,7 +165,7 @@ The Stopwatch widget class also extends `Static`. This class has a `compose()` m
#### 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.
- `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;
background: $boost;
height: 5;
- padding: 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.
- `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.
-- `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.
+- `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:
@@ -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.
-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
@@ -333,7 +335,7 @@ The following code will start or stop the stopwatches in response to clicking a
--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:
diff --git a/mkdocs.yml b/mkdocs.yml
index 78e6c97b3..ffc3f0f82 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -107,6 +107,7 @@ nav:
- "reference/index.md"
- "reference/message_pump.md"
- "reference/message.md"
+ - "reference/pilot.md"
- "reference/query.md"
- "reference/reactive.md"
- "reference/screen.md"
diff --git a/poetry.lock b/poetry.lock
index a8e8c639f..5aaf7bfbb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -18,7 +18,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
yarl = ">=1.0,<2.0"
[package.extras]
-speedups = ["aiodns", "brotli", "cchardet"]
+speedups = ["Brotli", "aiodns", "cchardet"]
[[package]]
name = "aiosignal"
@@ -59,10 +59,10 @@ optional = false
python-versions = ">=3.5"
[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"]
-docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "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", "sphinx-notfound-page", "zope.interface"]
+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 = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]]
name = "black"
@@ -120,11 +120,11 @@ optional = false
python-versions = ">=3.6.0"
[package.extras]
-unicode_backport = ["unicodedata2"]
+unicode-backport = ["unicodedata2"]
[[package]]
name = "click"
-version = "8.1.2"
+version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
@@ -136,11 +136,11 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
name = "colorama"
-version = "0.4.5"
+version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
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]]
name = "colored"
@@ -180,6 +180,17 @@ category = "dev"
optional = false
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]]
name = "filelock"
version = "3.8.0"
@@ -212,11 +223,11 @@ python-versions = "*"
python-dateutil = ">=2.8.1"
[package.extras]
-dev = ["twine", "markdown", "flake8", "wheel"]
+dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
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."
category = "dev"
optional = false
@@ -230,7 +241,7 @@ async = ["aiofiles (>=0.7,<1.0)"]
[[package]]
name = "identify"
-version = "2.5.6"
+version = "2.5.8"
description = "File identification library for Python"
category = "dev"
optional = false
@@ -260,9 +271,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[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"]
-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]]
name = "iniconfig"
@@ -339,8 +350,8 @@ typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""}
watchdog = ">=2.0"
[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)"]
+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]]
name = "mkdocs-autorefs"
@@ -472,6 +483,9 @@ category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+[package.dependencies]
+setuptools = "*"
+
[[package]]
name = "packaging"
version = "21.3"
@@ -500,8 +514,8 @@ optional = false
python-versions = ">=3.7"
[package.extras]
-docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
-test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
+docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
name = "pluggy"
@@ -535,14 +549,6 @@ pyyaml = ">=5.1"
toml = "*"
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]]
name = "pygments"
version = "2.13.0"
@@ -574,11 +580,11 @@ optional = false
python-versions = ">=3.6.8"
[package.extras]
-diagrams = ["railroad-diagrams", "jinja2"]
+diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
-version = "7.1.3"
+version = "7.2.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
@@ -587,12 +593,12 @@ python-versions = ">=3.7"
[package.dependencies]
attrs = ">=19.2.0"
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\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
-py = ">=1.8.2"
-tomli = ">=1.0.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
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\""}
[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]]
name = "pytest-cov"
@@ -642,7 +648,7 @@ pytest = ">=4.6"
toml = "*"
[package.extras]
-testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]]
name = "python-dateutil"
@@ -690,7 +696,7 @@ urllib3 = ">=1.21.1,<1.27"
[package.extras]
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]]
name = "rich"
@@ -708,6 +714,19 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9
[package.extras]
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]]
name = "six"
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"
[package.extras]
-brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
-secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+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)"]
[[package]]
name = "virtualenv"
-version = "20.16.5"
+version = "20.16.6"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
-distlib = ">=0.3.5,<1"
+distlib = ">=0.3.6,<1"
filelock = ">=3.4.1,<4"
importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""}
platformdirs = ">=2.4,<3"
[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)"]
[[package]]
@@ -828,15 +847,15 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[[package]]
name = "zipp"
-version = "3.9.0"
+version = "3.10.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
-docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
-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)"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
+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]
dev = ["aiohttp", "click", "msgpack"]
@@ -844,10 +863,98 @@ dev = ["aiohttp", "click", "msgpack"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
-content-hash = "84203bb5193474eb9204f4f808739cb25e61f02a38d0062ea2ea71d3703573c1"
+content-hash = "cfa35529900ee7fc7bca1e2a189f0240081bdbc75b501b25b394dfce66261c8b"
[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 = [
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
{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.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
]
-attrs = []
-black = []
+attrs = [
+ {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 = [
{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"},
]
-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 = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{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 = [
- {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"},
- {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"},
+ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
- {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
- {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+colored = [
+ {file = "colored-1.4.3.tar.gz", hash = "sha256:b7b48b9f40e8a65bbb54813d5d79dd008dc8b8c5638d5bbfd30fc5a82e6def7a"},
]
-colored = []
commonmark = [
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
]
-coverage = []
-distlib = []
-filelock = []
-frozenlist = []
+coverage = [
+ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
+ {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
+ {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 = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
]
-griffe = []
-identify = []
-idna = []
-importlib-metadata = []
+griffe = [
+ {file = "griffe-0.23.0-py3-none-any.whl", hash = "sha256:cfca5f523808109da3f8cfaa46e325fa2e5bef51120d1146e908c121b56475f0"},
+ {file = "griffe-0.23.0.tar.gz", hash = "sha256:a639e2968c8e27f56ebcc57f869a03cea7ac7e7f5684bd2429c665f761c4e7bd"},
+]
+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 = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{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.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 = [
{file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
]
-mkdocs-material = []
-mkdocs-material-extensions = []
+mkdocs-material = [
+ {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 = [
{file = "mkdocstrings-0.19.0-py3-none-any.whl", hash = "sha256:3217d510d385c961f69385a670b2677e68e07b5fea4a504d86bf54c006c87c7d"},
{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.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 = [
{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"},
]
-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 = [
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
{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.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 = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{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.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
-pre-commit = []
-py = [
- {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
- {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+pre-commit = [
+ {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
+ {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
+]
+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 = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{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 = [
{file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"},
{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 = [
{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"},
@@ -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-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{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-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"},
@@ -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.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
-requests = []
-rich = []
+requests = [
+ {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 = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
-syrupy = []
-time-machine = []
+syrupy = [
+ {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 = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{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.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
]
-typing-extensions = []
-urllib3 = []
-virtualenv = []
+typing-extensions = [
+ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
+ {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 = [
{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"},
@@ -1244,5 +1648,68 @@ watchdog = [
{file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"},
{file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"},
]
-yarl = []
-zipp = []
+yarl = [
+ {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"},
+]
diff --git a/pyproject.toml b/pyproject.toml
index dc6affad7..c4cbaa621 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,10 +33,10 @@ importlib-metadata = "^4.11.3"
typing-extensions = { version = "^4.0.0", python = "<3.10" }
# Dependencies below are required for devtools only
-aiohttp = { version = "^3.8.1", optional = true }
-click = {version = "8.1.2", optional = true}
-msgpack = { version = "^1.0.3", optional = true }
-nanoid = "^2.0.0"
+aiohttp = { version = ">=3.8.1", optional = true }
+click = {version = ">=8.1.2", optional = true}
+msgpack = { version = ">=1.0.3", optional = true }
+nanoid = ">=2.0.0"
[tool.poetry.extras]
dev = ["aiohttp", "click", "msgpack"]
diff --git a/sandbox/will/fr.py b/sandbox/will/fr.py
new file mode 100644
index 000000000..e82b895db
--- /dev/null
+++ b/sandbox/will/fr.py
@@ -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()
diff --git a/src/textual/__init__.py b/src/textual/__init__.py
index 3935489be..1a712e0c8 100644
--- a/src/textual/__init__.py
+++ b/src/textual/__init__.py
@@ -51,7 +51,9 @@ class Logger:
try:
app = active_app.get()
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:
return
diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py
index 2d89fad11..2bf706e0f 100644
--- a/src/textual/_arrange.py
+++ b/src/textual/_arrange.py
@@ -60,10 +60,9 @@ def arrange(
for dock_widget in dock_widgets:
edge = dock_widget.styles.dock
- fraction_unit = Fraction(
- size.height if edge in ("top", "bottom") else size.width
+ box_model = dock_widget._get_box_model(
+ 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 = int(widget_width_fraction) + margin.width
diff --git a/src/textual/_doc.py b/src/textual/_doc.py
index 17918e351..2d6bce67b 100644
--- a/src/textual/_doc.py
+++ b/src/textual/_doc.py
@@ -5,6 +5,7 @@ import shlex
from typing import Iterable
from textual.app import App
+from textual.pilot import Pilot
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]
_press = attrs.get("press", None)
- press = [*_press.split(",")] if _press else ["_"]
+ press = [*_press.split(",")] if _press else []
title = attrs.get("title")
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))
columns = int(attrs.get("columns", 80))
svg = take_svg_screenshot(
- None, path, press, title, terminal_size=(rows, columns)
+ None, path, press, title, terminal_size=(columns, rows)
)
finally:
os.chdir(cwd)
@@ -45,9 +46,9 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
def take_svg_screenshot(
app: App | None = None,
app_path: str | None = None,
- press: Iterable[str] = ("_",),
+ press: Iterable[str] = (),
title: str | None = None,
- terminal_size: tuple[int, int] = (24, 80),
+ terminal_size: tuple[int, int] = (80, 24),
) -> str:
"""
@@ -63,25 +64,29 @@ def take_svg_screenshot(
the screenshot was taken.
"""
- rows, columns = terminal_size
-
- os.environ["COLUMNS"] = str(columns)
- os.environ["LINES"] = str(rows)
if app is None:
+ assert app_path is not None
app = import_app(app_path)
+ assert app is not None
+
if title is None:
title = app.title
- app.run(
- quit_after=5,
- press=press or ["ctrl+c"],
+ async def auto_pilot(pilot: Pilot) -> None:
+ app = pilot.app
+ await pilot.press(*press)
+ svg = app.export_screenshot(title=title)
+ app.exit(svg)
+
+ svg = app.run(
headless=True,
- screenshot=True,
- screenshot_title=title,
+ auto_pilot=auto_pilot,
+ size=terminal_size,
)
- svg = app._screenshot
+ assert svg is not None
+
return svg
diff --git a/src/textual/_import_app.py b/src/textual/_import_app.py
index ee75227fb..e65e7fc85 100644
--- a/src/textual/_import_app.py
+++ b/src/textual/_import_app.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
+import sys
import runpy
import shlex
from pathlib import Path
@@ -30,7 +31,6 @@ def import_app(import_name: str) -> App:
import inspect
import importlib
- import sys
from textual.app import App, WINDOWS
@@ -45,8 +45,7 @@ def import_app(import_name: str) -> App:
except Exception as error:
raise AppFail(str(error))
- if "sys" in global_vars:
- global_vars["sys"].argv = [path, *argv]
+ sys.argv[:] = [path, *argv]
if name:
# User has given a name, use that
diff --git a/src/textual/_resolve.py b/src/textual/_resolve.py
index cf10dfcb5..3ace9759d 100644
--- a/src/textual/_resolve.py
+++ b/src/textual/_resolve.py
@@ -1,12 +1,23 @@
from __future__ import annotations
+import sys
from fractions import Fraction
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 .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(
dimensions: Sequence[Scalar],
@@ -71,3 +82,79 @@ def resolve(
]
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)
diff --git a/src/textual/app.py b/src/textual/app.py
index 038b821e3..9e96a4b94 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -1,6 +1,8 @@
from __future__ import annotations
import asyncio
+from asyncio import Task
+from contextlib import asynccontextmanager
import inspect
import io
import os
@@ -12,7 +14,18 @@ from contextlib import redirect_stderr, redirect_stdout
from datetime import datetime
from pathlib import Path, PurePath
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 ._ansi_sequences import SYNC_END, SYNC_START
@@ -51,7 +64,12 @@ from .widget import AwaitMount, Widget
if TYPE_CHECKING:
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()
WINDOWS = PLATFORM == "Windows"
@@ -89,6 +107,9 @@ ComposeResult = Iterable[Widget]
RenderResult = RenderableType
+AutopilotCallbackType: TypeAlias = "Callable[[Pilot], Coroutine[Any, Any, None]]"
+
+
class AppError(Exception):
pass
@@ -170,7 +191,7 @@ class App(Generic[ReturnType], DOMNode):
if no_color is not None:
self._filter = Monochrome()
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,
highlight=False,
emoji=False,
@@ -241,6 +262,11 @@ class App(Generic[ReturnType], DOMNode):
)
self._screenshot: str | None = None
+ @property
+ def return_value(self) -> ReturnType | None:
+ """Get the return type."""
+ return self._return_value
+
def animate(
self,
attribute: str,
@@ -295,7 +321,7 @@ class App(Generic[ReturnType], DOMNode):
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
def screen_stack(self) -> list[Screen]:
@@ -314,7 +340,7 @@ class App(Generic[ReturnType], DOMNode):
result (ReturnType | None, optional): Return value. Defaults to None.
"""
self._return_value = result
- self._close_messages_no_wait()
+ self.post_message_no_wait(messages.ExitApp(sender=self))
@property
def focused(self) -> Widget | None:
@@ -418,7 +444,11 @@ class App(Generic[ReturnType], DOMNode):
Returns:
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
def log(self) -> Logger:
@@ -500,10 +530,11 @@ class App(Generic[ReturnType], DOMNode):
to use app title. Defaults to None.
"""
-
+ assert self._driver is not None, "App must be running"
+ width, height = self.size
console = Console(
- width=self.console.width,
- height=self.console.height,
+ width=width,
+ height=height,
file=io.StringIO(),
force_terminal=True,
color_system="truecolor",
@@ -567,95 +598,170 @@ class App(Generic[ReturnType], DOMNode):
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(
self,
*,
- quit_after: float | None = None,
headless: bool = False,
- press: Iterable[str] | None = None,
- screenshot: bool = False,
- screenshot_title: str | None = None,
+ size: tuple[int, int] | None = None,
+ auto_pilot: AutopilotCallbackType | None = None,
) -> ReturnType | None:
- """The main entry point for apps.
+ """Run the app.
Args:
- quit_after (float | None, optional): Quit after a given number of seconds, or None
- to run forever. Defaults to None.
- headless (bool, optional): Run in "headless" mode (don't write to stdout).
- press (str, optional): An iterable of keys to simulate being pressed.
- 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.
+ 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: 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:
- if quit_after is not None:
- self.set_timer(quit_after, self.shutdown)
- if press is not None:
- app = self
-
- 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()
+ """Run the app."""
+ await self.run_async(
+ headless=headless,
+ size=size,
+ auto_pilot=auto_pilot,
+ )
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:
@@ -664,8 +770,7 @@ class App(Generic[ReturnType], DOMNode):
# However, this works with Python<3.10:
event_loop = asyncio.get_event_loop()
event_loop.run_until_complete(run_app())
-
- return self._return_value
+ return self.return_value
async def _on_css_change(self) -> None:
"""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:
"""Get an installed screen.
- 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).
@@ -774,10 +877,30 @@ class App(Generic[ReturnType], DOMNode):
raise KeyError(f"No screen called {screen!r} installed") from None
else:
next_screen = screen
- if not next_screen.is_running:
- self._register(self, 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:
"""Handle the replaced screen.
@@ -795,19 +918,20 @@ class App(Generic[ReturnType], DOMNode):
self.log.system(f"{screen} REMOVED")
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.
Args:
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.post_message_no_wait(events.ScreenResume(self))
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.
Args:
@@ -816,12 +940,14 @@ class App(Generic[ReturnType], DOMNode):
"""
if self.screen is not screen:
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.post_message_no_wait(events.ScreenResume(self))
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.
Args:
@@ -833,7 +959,7 @@ class App(Generic[ReturnType], DOMNode):
ScreenError: If the screen can't be installed.
Returns:
- str: The name of the screen
+ AwaitMount: An awaitable that awaits the mounting of the screen and its children.
"""
if name is None:
name = nanoid.generate()
@@ -844,9 +970,9 @@ class App(Generic[ReturnType], DOMNode):
"Can't install screen; {screen!r} has already been installed"
)
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}")
- return name
+ return await_mount
def uninstall_screen(self, screen: Screen | str) -> str | None:
"""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()
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:
self._set_active()
@@ -1038,22 +1167,31 @@ class App(Generic[ReturnType], DOMNode):
self.log.system("[b green]STARTED[/]", self.css_monitor)
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:
- await self._dispatch_message(events.Compose(sender=self))
- await self._dispatch_message(events.Mount(sender=self))
+ try:
+ 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:
- self._mounted_event.set()
-
- 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()
+ await self._ready()
+ await invoke_ready_callback()
self._running = True
@@ -1067,7 +1205,6 @@ class App(Generic[ReturnType], DOMNode):
await timer.stop()
await self.animator.stop()
- await self._close_all()
self._running = True
try:
@@ -1077,13 +1214,13 @@ class App(Generic[ReturnType], DOMNode):
driver: Driver
driver_class = cast(
"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()
try:
- if self.is_headless:
+ if headless:
await run_process_messages()
else:
if self.devtools is not None:
@@ -1105,11 +1242,6 @@ class App(Generic[ReturnType], DOMNode):
driver.stop_application_mode()
except Exception as 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:
pass
@@ -1134,12 +1266,17 @@ class App(Generic[ReturnType], DOMNode):
"""Used by docs plugin."""
svg = self.export_screenshot(title=screenshot_title)
self._screenshot = svg # type: ignore
- await self.shutdown()
+ self.exit()
self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer")
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)
def _on_idle(self) -> None:
@@ -1218,8 +1355,10 @@ class App(Generic[ReturnType], DOMNode):
parent (Widget): The parent of the Widget.
widget (Widget): The Widget to start.
"""
+
widget._attach(parent)
widget._start_messages()
+ self.app._registry.add(widget)
def is_mounted(self, widget: Widget) -> bool:
"""Check if a widget is mounted.
@@ -1233,17 +1372,43 @@ class App(Generic[ReturnType], DOMNode):
return widget in self._registry
async def _close_all(self) -> None:
- while self._registry:
- child = self._registry.pop()
+ """Close all message pumps."""
+
+ # 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()
- async def shutdown(self):
- await self._disconnect_devtools()
+ async def _shutdown(self) -> None:
driver = self._driver
+ self._running = False
if driver is not None:
driver.disable_input()
+ await self._close_all()
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:
if self._screen_stack:
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],
)
- for child in remove_widgets:
- await child._close_messages()
- self._unregister(child)
+ await self._prune_node(widget)
+
if parent is not None:
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:
await self.check_bindings(key)
async def action_quit(self) -> None:
"""Quit the app as soon as possible."""
- await self.shutdown()
+ self.exit()
async def action_bang(self) -> None:
1 / 0
diff --git a/src/textual/box_model.py b/src/textual/box_model.py
index 7271bef54..2bd93aa00 100644
--- a/src/textual/box_model.py
+++ b/src/textual/box_model.py
@@ -20,7 +20,8 @@ def get_box_model(
styles: StylesBase,
container: Size,
viewport: Size,
- fraction_unit: Fraction,
+ width_fraction: Fraction,
+ height_fraction: Fraction,
get_content_width: Callable[[Size, Size], int],
get_content_height: Callable[[Size, Size, int], int],
) -> BoxModel:
@@ -30,6 +31,8 @@ def get_box_model(
styles (StylesBase): Styles object.
container (Size): The size of the widget container.
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_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
styles_width = styles.width
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:
content_width -= gutter.width
@@ -71,14 +74,14 @@ def get_box_model(
if styles.min_width is not None:
# Restrict to minimum width, if set
min_width = styles.min_width.resolve_dimension(
- content_container, viewport, fraction_unit
+ content_container, viewport, width_fraction
)
content_width = max(content_width, min_width)
if styles.max_width is not None:
# Restrict to maximum width, if set
max_width = styles.max_width.resolve_dimension(
- content_container, viewport, fraction_unit
+ content_container, viewport, width_fraction
)
if is_border_box:
max_width -= gutter.width
@@ -98,7 +101,7 @@ def get_box_model(
styles_height = styles.height
# Explicit height set
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:
content_height -= gutter.height
@@ -106,14 +109,14 @@ def get_box_model(
if styles.min_height is not None:
# Restrict to minimum height, if set
min_height = styles.min_height.resolve_dimension(
- content_container, viewport, fraction_unit
+ content_container, viewport, height_fraction
)
content_height = max(content_height, min_height)
if styles.max_height is not None:
# Restrict maximum height, if set
max_height = styles.max_height.resolve_dimension(
- content_container, viewport, fraction_unit
+ content_container, viewport, height_fraction
)
content_height = min(content_height, max_height)
diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py
index 3b1df1f4b..414575d82 100644
--- a/src/textual/cli/cli.py
+++ b/src/textual/cli/cli.py
@@ -4,6 +4,7 @@ from __future__ import annotations
import click
from importlib_metadata import version
+from textual.pilot import Pilot
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)
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:
from rich.console import Console
diff --git a/src/textual/demo.css b/src/textual/demo.css
index 08a2e4e9b..fd968f51e 100644
--- a/src/textual/demo.css
+++ b/src/textual/demo.css
@@ -114,13 +114,14 @@ DarkSwitch {
}
DarkSwitch .label {
-
+ width: 1fr;
padding: 1 2;
color: $text-muted;
}
DarkSwitch Checkbox {
background: $boost;
+ dock: left;
}
diff --git a/src/textual/driver.py b/src/textual/driver.py
index 559c5e014..5e470b697 100644
--- a/src/textual/driver.py
+++ b/src/textual/driver.py
@@ -13,14 +13,25 @@ if TYPE_CHECKING:
class Driver(ABC):
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:
self.console = console
self._target = target
self._debug = debug
+ self._size = size
self._loop = asyncio.get_running_loop()
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:
asyncio.run_coroutine_threadsafe(
self._target.post_message(event), loop=self._loop
diff --git a/src/textual/drivers/headless_driver.py b/src/textual/drivers/headless_driver.py
index cdde957b9..87a0940e7 100644
--- a/src/textual/drivers/headless_driver.py
+++ b/src/textual/drivers/headless_driver.py
@@ -9,7 +9,13 @@ from .. import events
class HeadlessDriver(Driver):
"""A do-nothing driver for testing."""
+ @property
+ def is_headless(self) -> bool:
+ return True
+
def _get_terminal_size(self) -> tuple[int, int]:
+ if self._size is not None:
+ return self._size
width: int | None = 80
height: int | None = 25
import shutil
diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py
index e8c7bd00a..f0e75e71e 100644
--- a/src/textual/drivers/linux_driver.py
+++ b/src/textual/drivers/linux_driver.py
@@ -30,9 +30,14 @@ class LinuxDriver(Driver):
"""Powers display and input for Linux / MacOS"""
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:
- super().__init__(console, target, debug)
+ super().__init__(console, target, debug=debug, size=size)
self.fileno = sys.stdin.fileno()
self.attrs_before: list[Any] | None = None
self.exit_event = Event()
diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py
index fb51973ea..b14af7ab5 100644
--- a/src/textual/drivers/windows_driver.py
+++ b/src/textual/drivers/windows_driver.py
@@ -18,9 +18,14 @@ class WindowsDriver(Driver):
"""Powers display and input for Windows."""
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:
- super().__init__(console, target, debug)
+ super().__init__(console, target, debug=debug, size=size)
self.in_fileno = sys.stdin.fileno()
self.out_fileno = sys.stdout.fileno()
diff --git a/src/textual/events.py b/src/textual/events.py
index ee84b929f..046ded120 100644
--- a/src/textual/events.py
+++ b/src/textual/events.py
@@ -119,10 +119,14 @@ class Compose(Event, bubble=False, verbose=True):
"""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."""
+class Unmount(Mount, bubble=False, verbose=False):
+ """Sent when a widget is unmounted and may not longer receive messages."""
+
+
class Remove(Event, bubble=False):
"""Sent to a widget to ask it to remove itself from the DOM."""
diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py
index ea9466461..cff1de36d 100644
--- a/src/textual/layouts/grid.py
+++ b/src/textual/layouts/grid.py
@@ -145,9 +145,7 @@ class GridLayout(Layout):
y2, cell_height = rows[min(max_row, row + row_span)]
cell_size = Size(cell_width + x2 - x, cell_height + y2 - y)
width, height, margin = widget._get_box_model(
- cell_size,
- viewport,
- fraction_unit,
+ cell_size, viewport, fraction_unit, fraction_unit
)
region = (
Region(x, y, int(width + margin.width), int(height + margin.height))
diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py
index 1216beb4e..210ee95d3 100644
--- a/src/textual/layouts/horizontal.py
+++ b/src/textual/layouts/horizontal.py
@@ -1,12 +1,11 @@
from __future__ import annotations
from fractions import Fraction
-from typing import cast
-from textual.geometry import Size, Region
-from textual._layout import ArrangeResult, Layout, WidgetPlacement
-
-from textual.widget import Widget
+from .._resolve import resolve_box_models
+from ..geometry import Size, Region
+from .._layout import ArrangeResult, Layout, WidgetPlacement
+from ..widget import Widget
class HorizontalLayout(Layout):
@@ -22,20 +21,16 @@ class HorizontalLayout(Layout):
placements: list[WidgetPlacement] = []
add_placement = placements.append
-
x = max_height = Fraction(0)
parent_size = parent.outer_size
- styles = [child.styles for child in children if child.styles.width is not None]
- total_fraction = sum(
- [int(style.width.value) for style in styles if style.width.is_fraction]
+ box_models = resolve_box_models(
+ [child.styles.width for child in children],
+ 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 = [
max((box1.margin.right, box2.margin.left))
diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py
index 9460bf0db..ccdcd1be3 100644
--- a/src/textual/layouts/vertical.py
+++ b/src/textual/layouts/vertical.py
@@ -3,6 +3,7 @@ from __future__ import annotations
from fractions import Fraction
from typing import TYPE_CHECKING
+from .._resolve import resolve_box_models
from ..geometry import Region, Size
from .._layout import ArrangeResult, Layout, WidgetPlacement
@@ -21,19 +22,15 @@ class VerticalLayout(Layout):
placements: list[WidgetPlacement] = []
add_placement = placements.append
-
parent_size = parent.outer_size
- styles = [child.styles for child in children if child.styles.height is not None]
- total_fraction = sum(
- [int(style.height.value) for style in styles if style.height.is_fraction]
+ box_models = resolve_box_models(
+ [child.styles.height for child in children],
+ 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 = [
max((box1.margin.bottom, box2.margin.top))
diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py
index 7d7712a81..4ede28b75 100644
--- a/src/textual/message_pump.py
+++ b/src/textual/message_pump.py
@@ -155,7 +155,9 @@ class MessagePump(metaclass=MessagePumpMeta):
return self._pending_message
finally:
self._pending_message = None
+
message = await self._message_queue.get()
+
if message is None:
self._closed = True
raise MessagePumpClosed("The message pump is now closed")
@@ -266,8 +268,11 @@ class MessagePump(metaclass=MessagePumpMeta):
self.app.screen._invoke_later(message.callback)
def _close_messages_no_wait(self) -> None:
- """Request the message queue to exit."""
- self._message_queue.put_nowait(None)
+ """Request the message queue to immediately exit."""
+ 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:
"""Close message queue, and optionally wait for queue to finish processing."""
@@ -278,6 +283,7 @@ class MessagePump(metaclass=MessagePumpMeta):
for timer in stop_timers:
await timer.stop()
self._timers.clear()
+ await self._message_queue.put(events.Unmount(sender=self))
await self._message_queue.put(None)
if self._task is not None and asyncio.current_task() != self._task:
# Ensure everything is closed before returning
@@ -285,7 +291,8 @@ class MessagePump(metaclass=MessagePumpMeta):
def _start_messages(self) -> None:
"""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:
self._running = True
@@ -370,8 +377,6 @@ class MessagePump(metaclass=MessagePumpMeta):
self.app._handle_exception(error)
break
- log("CLOSED", self)
-
async def _dispatch_message(self, message: Message) -> None:
"""Dispatch a message received from the message queue.
@@ -424,6 +429,7 @@ class MessagePump(metaclass=MessagePumpMeta):
handler_name = message._handler_name
# Look through the MRO to find a handler
+ dispatched = False
for cls, method in self._get_dispatch_methods(handler_name, message):
log.event.verbosity(message.verbose)(
message,
@@ -431,7 +437,10 @@ class MessagePump(metaclass=MessagePumpMeta):
self,
f"method=<{cls.__name__}.{handler_name}>",
)
+ dispatched = True
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)
if message.bubble and self._parent and not message._stop_propagation:
diff --git a/src/textual/messages.py b/src/textual/messages.py
index 2b1ff4792..f23a1a8a8 100644
--- a/src/textual/messages.py
+++ b/src/textual/messages.py
@@ -13,6 +13,16 @@ if TYPE_CHECKING:
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
class Update(Message, verbose=True):
def __init__(self, sender: MessagePump, widget: Widget):
diff --git a/src/textual/pilot.py b/src/textual/pilot.py
new file mode 100644
index 000000000..6f3f046a4
--- /dev/null
+++ b/src/textual/pilot.py
@@ -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)
diff --git a/src/textual/reactive.py b/src/textual/reactive.py
index c237955b4..25e5f4b0a 100644
--- a/src/textual/reactive.py
+++ b/src/textual/reactive.py
@@ -5,7 +5,6 @@ from inspect import isawaitable
from typing import TYPE_CHECKING, Any, Callable, Generic, Type, TypeVar, Union
from weakref import WeakSet
-
from . import events
from ._callback import count_parameters, invoke
from ._types import MessageTarget
@@ -16,7 +15,6 @@ if TYPE_CHECKING:
Reactable = Union[Widget, App]
-
ReactiveType = TypeVar("ReactiveType")
@@ -37,7 +35,7 @@ class Reactive(Generic[ReactiveType]):
layout (bool, optional): Perform a layout on change. Defaults to False.
repaint (bool, optional): Perform a repaint on change. Defaults to True.
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__(
@@ -47,11 +45,13 @@ class Reactive(Generic[ReactiveType]):
layout: bool = False,
repaint: bool = True,
init: bool = False,
+ always_update: bool = False,
) -> None:
self._default = default
self._layout = layout
self._repaint = repaint
self._init = init
+ self._always_update = always_update
@classmethod
def init(
@@ -60,6 +60,7 @@ class Reactive(Generic[ReactiveType]):
*,
layout: bool = False,
repaint: bool = True,
+ always_update: bool = False,
) -> Reactive:
"""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.
layout (bool, optional): Perform a layout on change. Defaults to False.
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:
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
def var(
@@ -153,7 +160,7 @@ class Reactive(Generic[ReactiveType]):
if callable(validate_function) and not first_set:
value = validate_function(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
setattr(obj, f"__first_set_{self.internal_name}", False)
# Store the internal value
@@ -259,7 +266,7 @@ class reactive(Reactive[ReactiveType]):
layout (bool, optional): Perform a layout on change. Defaults to False.
repaint (bool, optional): Perform a repaint on change. 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__(
@@ -269,8 +276,15 @@ class reactive(Reactive[ReactiveType]):
layout: bool = False,
repaint: bool = True,
init: bool = True,
+ always_update: bool = False,
) -> 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]):
diff --git a/src/textual/screen.py b/src/textual/screen.py
index 70b971125..f0d6752b9 100644
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -12,7 +12,6 @@ from ._callback import invoke
from ._compositor import Compositor, MapGeometry
from .timer import Timer
from ._types import CallbackType
-from .dom import DOMNode
from .geometry import Offset, Region, Size
from .reactive import Reactive
from .renderables.blank import Blank
@@ -61,7 +60,12 @@ class Screen(Widget):
@property
def is_current(self) -> bool:
"""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
def update_timer(self) -> Timer:
diff --git a/src/textual/widget.py b/src/textual/widget.py
index 363c1f518..13fd2ab5b 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -82,11 +82,13 @@ class AwaitMount:
def __await__(self) -> Generator[None, None, None]:
async def await_mount() -> None:
- aws = [
- create_task(widget._mounted_event.wait()) for widget in self._widgets
- ]
- if aws:
- await wait(aws)
+ if self._widgets:
+ aws = [
+ create_task(widget._mounted_event.wait())
+ for widget in self._widgets
+ ]
+ if aws:
+ await wait(aws)
return await_mount().__await__()
@@ -359,6 +361,20 @@ class Widget(DOMNode):
"""Clear arrangement cache, forcing a new arrange operation."""
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:
"""Mount child widgets (making this widget a container).
@@ -409,14 +425,19 @@ class Widget(DOMNode):
)
def _get_box_model(
- self, container: Size, viewport: Size, fraction_unit: Fraction
+ self,
+ container: Size,
+ viewport: Size,
+ width_fraction: Fraction,
+ height_fraction: Fraction,
) -> BoxModel:
"""Process the box model for this widget.
Args:
container (Size): The size of the container widget (with a layout)
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:
BoxModel: The size and margin for this widget.
@@ -425,7 +446,8 @@ class Widget(DOMNode):
self.styles,
container,
viewport,
- fraction_unit,
+ width_fraction,
+ height_fraction,
self.get_content_width,
self.get_content_height,
)
@@ -587,6 +609,7 @@ class Widget(DOMNode):
Returns:
ScrollBar: ScrollBar Widget.
"""
+
from .scrollbar import ScrollBar
if self._horizontal_scrollbar is not None:
@@ -595,13 +618,12 @@ class Widget(DOMNode):
vertical=False, name="horizontal", thickness=self.scrollbar_size_horizontal
)
self._horizontal_scrollbar.display = False
-
self.app._start_widget(self, scroll_bar)
return scroll_bar
def _refresh_scrollbars(self) -> None:
"""Refresh scrollbar visibility."""
- if not self.is_scrollable:
+ if not self.is_scrollable or not self.container_size:
return
styles = self.styles
@@ -1930,8 +1952,13 @@ class Widget(DOMNode):
async def handle_key(self, event: events.Key) -> bool:
return await self.dispatch_key(event)
- async def _on_compose(self, event: events.Compose) -> None:
- widgets = list(self.compose())
+ async def _on_compose(self) -> None:
+ 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)
def _on_mount(self, event: events.Mount) -> None:
diff --git a/tests/css/test_styles.py b/tests/css/test_styles.py
index a7c574ecc..8819a6839 100644
--- a/tests/css/test_styles.py
+++ b/tests/css/test_styles.py
@@ -18,8 +18,6 @@ from textual.css.styles import Styles, RenderStyles
from textual.dom import DOMNode
from textual.widget import Widget
-from tests.utilities.test_app import AppTest
-
def test_styles_reset():
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):
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
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index 7739c6bc9..d9eb39417 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -4913,6 +4913,162 @@
'''
# ---
+# name: test_fr_units
+ '''
+
+
+ '''
+# ---
# name: test_grid_layout_basic
'''