diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 27f36d406..3693892fc 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -40,7 +40,7 @@ jobs:
path: .venv
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
- run: poetry install --extras "dev"
+ run: poetry install
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
# - name: Typecheck with mypy
# run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 546efcc16..a0e7b12de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,8 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
-## Unreleased
-
+## [0.29.0] - 2023-07-03
### Added
@@ -15,14 +14,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `default` parameter to `DataTable.add_column` for populating existing rows https://github.com/Textualize/textual/pull/2836
- Added can-focus pseudo-class to target widgets that may receive focus
-
### Fixed
- Fixed crash when columns were added to populated `DataTable` https://github.com/Textualize/textual/pull/2836
- Fixed issues with opacity on Screens https://github.com/Textualize/textual/issues/2616
- Fixed style problem with selected selections in a non-focused selection list https://github.com/Textualize/textual/issues/2768
-
## [0.28.1] - 2023-06-20
### Fixed
@@ -1098,6 +1095,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling
+[0.29.0]: https://github.com/Textualize/textual/compare/v0.28.1...v0.29.0
[0.28.1]: https://github.com/Textualize/textual/compare/v0.28.0...v0.28.1
[0.28.0]: https://github.com/Textualize/textual/compare/v0.27.0...v0.28.0
[0.27.0]: https://github.com/Textualize/textual/compare/v0.26.0...v0.27.0
diff --git a/FAQ.md b/FAQ.md
index 52442e527..260bdc58a 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -28,7 +28,7 @@ You likely have an older version of Textual. You can install the latest version
The following should do it:
```
-pip install "textual[dev]" -U
+pip install textual-dev -U
```
diff --git a/README.md b/README.md
index d15d8cd1a..b2b518b0a 100644
--- a/README.md
+++ b/README.md
@@ -13,12 +13,12 @@ Textual is a *Rapid Application Development* framework for Python.
Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and (coming soon) a web browser!
-
+
🎬 Demonstration
-
+
A quick run through of some Textual features.
-
+
https://user-images.githubusercontent.com/554369/197355913-65d3c125-493d-4c05-a590-5311f16c40ff.mov
@@ -32,7 +32,7 @@ https://user-images.githubusercontent.com/554369/197355913-65d3c125-493d-4c05-a5
Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with an API inspired by modern web development.
-On modern terminal software (installed by default on most systems), Textual apps can use **16.7 million** colors with mouse support and smooth flicker-free animation. A powerful layout engine and re-usable components makes it possible to build apps that rival the desktop and web experience.
+On modern terminal software (installed by default on most systems), Textual apps can use **16.7 million** colors with mouse support and smooth flicker-free animation. A powerful layout engine and re-usable components makes it possible to build apps that rival the desktop and web experience.
## Compatibility
@@ -43,10 +43,16 @@ Textual runs on Linux, macOS, and Windows. Textual requires Python 3.7 or above.
Install Textual via pip:
```
-pip install "textual[dev]"
+pip install textual
```
-The addition of `[dev]` installs Textual development tools. See the [docs](https://textual.textualize.io/getting_started/) if you need help getting started.
+If you plan on developing Textual apps, you should also install the development tools with the following command:
+
+```
+pip install textual-dev
+```
+
+See the [docs](https://textual.textualize.io/getting_started/) if you need help getting started.
## Demo
@@ -82,12 +88,12 @@ https://user-images.githubusercontent.com/554369/197188237-88d3f7e4-4e5f-40b5-b9
-
+
📷 Calculator
-
+
This is [calculator.py](https://github.com/Textualize/textual/blob/main/examples/calculator.py) which demonstrates Textual grid layouts.
-
+

@@ -97,7 +103,7 @@ This is [calculator.py](https://github.com/Textualize/textual/blob/main/examples
This is the Stopwatch example from the [tutorial](https://textual.textualize.io/tutorial/).
-
+
https://user-images.githubusercontent.com/554369/197360718-0c834ef5-6285-4d37-85cf-23eed4aa56c5.mov
@@ -112,12 +118,12 @@ https://user-images.githubusercontent.com/554369/197360718-0c834ef5-6285-4d37-85
The `textual` command has a few sub-commands to preview Textual styles.
-
+
🎬 Easing reference
-
+
This is the *easing* reference which demonstrates the easing parameter on animation, with both movement and opacity. You can run it with the following command:
-
+
```bash
textual easing
```
@@ -128,12 +134,12 @@ https://user-images.githubusercontent.com/554369/196157100-352852a6-2b09-4dc8-a8
-
+
🎬 Borders reference
-
+
This is the borders reference which demonstrates some of the borders styles in Textual. You can run it with the following command:
-
+
```bash
textual borders
```
@@ -141,16 +147,16 @@ textual borders
https://user-images.githubusercontent.com/554369/196158235-4b45fb78-053d-4fd5-b285-e09b4f1c67a8.mov
-
+
-
+
🎬 Colors reference
-
+
This is a reference for Textual's color design system.
-
+
```bash
textual colors
```
@@ -161,6 +167,5 @@ https://user-images.githubusercontent.com/554369/197357417-2d407aac-8969-44d3-82
-
-
+
diff --git a/docs/blog/posts/release0-29-0.md b/docs/blog/posts/release0-29-0.md
new file mode 100644
index 000000000..c14466cf7
--- /dev/null
+++ b/docs/blog/posts/release0-29-0.md
@@ -0,0 +1,31 @@
+---
+draft: false
+date: 2023-07-03
+categories:
+ - Release
+title: "Textual 0.27.0 refactors dev tools"
+authors:
+ - willmcgugan
+---
+
+# Textual 0.27.0 refactors dev tools
+
+It's been a slow week or two at Textualize, with Textual devs taking well-earned annual leave, but we still managed to get a new version out.
+
+
+
+Version 0.27.0 has shipped with a number of fixes (see the [release notes](https://github.com/Textualize/textual/releases/tag/v0.29.0) for details), but I'd like to use this post to explain a change we made to how Textual developer tools are distributed.
+
+Previously if you installed `textual[dev]` you would get the Textual dev tools plus the library itself. If you were distributing Textual apps and didn't need the developer tools you could drop the `[dev]`.
+
+We did this because the less dependencies a package has, the fewer installation issues you can expect to get in the future. And Textual is surprisingly lean if you only need to *run* apps, and not build them.
+
+Alas, this wasn't quite as elegant solution as we hoped. The dependencies defined in extras wouldn't install commands, so `textual` was bundled with the core library. This meant that if you installed the Textual package *without* the `[dev]` you would still get the `textual` command on your path but it wouldn't run.
+
+We solved this by creating two packages: `textual` contains the core library (with minimal dependencies) and `textual-dev` contains the developer tools. If you are building Textual apps, you should install both as follows:
+
+```
+pip install textual textual-dev
+```
+
+That's the only difference. If you run in to any issues feel free to ask on the [Discord server](https://discord.gg/Enf6Z3qhVr)!
diff --git a/docs/examples/getting_started/console.py b/docs/examples/getting_started/console.py
index 507e6aac7..0308ed689 100644
--- a/docs/examples/getting_started/console.py
+++ b/docs/examples/getting_started/console.py
@@ -2,8 +2,9 @@
Simulates a screenshot of the Textual devtools
"""
+from textual_dev.renderables import DevConsoleHeader
+
from textual.app import App
-from textual.devtools.renderables import DevConsoleHeader
from textual.widgets import Static
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 82739d337..9e0bf3317 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -20,20 +20,28 @@ Textual requires Python 3.7 or later (if you have a choice, pick the most recent
## Installation
-You can install Textual via PyPI.
-
-If you plan on developing Textual apps, then you should install `textual[dev]`. The `[dev]` part installs a few extra dependencies for development.
-
-```
-pip install "textual[dev]"
-```
-
-If you only plan on _running_ Textual apps, then you can drop the `[dev]` part:
+You can install Textual via PyPI, with the following command:
```
pip install textual
```
+If you plan on developing Textual apps, you should also install textual developer tools:
+
+```
+pip install textual-dev
+```
+
+### Textual CLI
+
+If you installed the developer tools you should have access to the `textual` command. There are a number of sub-commands available which will aid you in building Textual apps. Run the following for a list of the available commands:
+
+```bash
+textual --help
+```
+
+See [devtools](guide/devtools.md) for more about the `textual` command.
+
## Demo
Once you have Textual installed, run the following to get an impression of what it can do:
@@ -79,15 +87,7 @@ python code_browser.py ../
```
-## Textual CLI
-If you installed the dev dependencies you have access to the `textual` CLI command. There are a number of sub-commands which will aid you in building Textual apps.
-
-```bash
-textual --help
-```
-
-See [devtools](guide/devtools.md) for more about the `textual` command.
## Need help?
diff --git a/docs/guide/animation.md b/docs/guide/animation.md
index 99b9783a4..1e9724dbe 100644
--- a/docs/guide/animation.md
+++ b/docs/guide/animation.md
@@ -72,7 +72,7 @@ You can specify which easing method to use via the `easing` parameter on the `an
!!! note
- The `textual easing` preview requires the `dev` extras to be installed (using `pip install textual[dev]`).
+ The `textual easing` preview requires the `textual-dev` package to be installed (using `pip install textual-dev`).
## Completion callbacks
diff --git a/poetry.lock b/poetry.lock
index fcde137db..b79a18682 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,1127 +1,13 @@
+# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
+
[[package]]
name = "aiohttp"
version = "3.8.4"
description = "Async http client/server framework (asyncio)"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-aiosignal = ">=1.1.2"
-async-timeout = ">=4.0.0a3,<5.0"
-asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""}
-attrs = ">=17.3.0"
-charset-normalizer = ">=2.0,<4.0"
-frozenlist = ">=1.1.1"
-multidict = ">=4.5,<7.0"
-typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
-yarl = ">=1.0,<2.0"
-
-[package.extras]
-speedups = ["Brotli", "aiodns", "cchardet"]
-
-[[package]]
-name = "aiosignal"
-version = "1.3.1"
-description = "aiosignal: a list of registered asynchronous callbacks"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-frozenlist = ">=1.1.0"
-
-[[package]]
-name = "anyio"
-version = "3.7.0"
-description = "High level compatibility layer for multiple asynchronous event loop implementations"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
-idna = ">=2.8"
-sniffio = ">=1.1"
-typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
-
-[package.extras]
-doc = ["Sphinx (>=6.1.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "sphinxcontrib-jquery"]
-test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
-trio = ["trio (<0.22)"]
-
-[[package]]
-name = "async-timeout"
-version = "4.0.2"
-description = "Timeout context manager for asyncio programs"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "asynctest"
-version = "0.13.0"
-description = "Enhance the standard unittest package with features for testing asyncio libraries"
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "attrs"
-version = "23.1.0"
-description = "Classes Without Boilerplate"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
-
-[package.extras]
-cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
-dev = ["attrs[docs,tests]", "pre-commit"]
-docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
-tests = ["attrs[tests-no-zope]", "zope-interface"]
-tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-
-[[package]]
-name = "black"
-version = "23.3.0"
-description = "The uncompromising code formatter."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-click = ">=8.0.0"
-mypy-extensions = ">=0.4.3"
-packaging = ">=22.0"
-pathspec = ">=0.9.0"
-platformdirs = ">=2"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
-typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
-
-[package.extras]
-colorama = ["colorama (>=0.4.3)"]
-d = ["aiohttp (>=3.7.4)"]
-jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
-uvloop = ["uvloop (>=0.15.2)"]
-
-[[package]]
-name = "cached-property"
-version = "1.5.2"
-description = "A decorator for caching properties in classes."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "certifi"
-version = "2023.5.7"
-description = "Python package for providing Mozilla's CA Bundle."
category = "dev"
optional = false
python-versions = ">=3.6"
-
-[[package]]
-name = "cfgv"
-version = "3.3.1"
-description = "Validate configuration and produce human readable error messages."
-category = "dev"
-optional = false
-python-versions = ">=3.6.1"
-
-[[package]]
-name = "charset-normalizer"
-version = "3.1.0"
-description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
-optional = false
-python-versions = ">=3.7.0"
-
-[[package]]
-name = "click"
-version = "8.1.3"
-description = "Composable command line interface toolkit"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-colorama = {version = "*", markers = "platform_system == \"Windows\""}
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-description = "Cross-platform colored terminal text."
-category = "main"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
-
-[[package]]
-name = "colored"
-version = "1.4.4"
-description = "Simple library for color and formatting to terminal"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "coverage"
-version = "7.2.7"
-description = "Code coverage measurement for Python"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-toml = ["tomli"]
-
-[[package]]
-name = "distlib"
-version = "0.3.6"
-description = "Distribution utilities"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "exceptiongroup"
-version = "1.1.1"
-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.12.2"
-description = "A platform independent file lock."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
-
-[[package]]
-name = "frozenlist"
-version = "1.3.3"
-description = "A list-like structure which implements collections.abc.MutableSequence"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "ghp-import"
-version = "2.1.0"
-description = "Copy your docs directly to the gh-pages branch."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-python-dateutil = ">=2.8.1"
-
-[package.extras]
-dev = ["flake8", "markdown", "twine", "wheel"]
-
-[[package]]
-name = "gitdb"
-version = "4.0.10"
-description = "Git Object Database"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-smmap = ">=3.0.1,<6"
-
-[[package]]
-name = "gitpython"
-version = "3.1.31"
-description = "GitPython is a Python library used to interact with Git repositories"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-gitdb = ">=4.0.1,<5"
-typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "griffe"
-version = "0.29.1"
-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
-python-versions = ">=3.7"
-
-[package.dependencies]
-cached-property = {version = "*", markers = "python_version < \"3.8\""}
-colorama = ">=0.4"
-
-[[package]]
-name = "h11"
-version = "0.14.0"
-description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "httpcore"
-version = "0.16.3"
-description = "A minimal low-level HTTP client."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-anyio = ">=3.0,<5.0"
-certifi = "*"
-h11 = ">=0.13,<0.15"
-sniffio = ">=1.0.0,<2.0.0"
-
-[package.extras]
-http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (>=1.0.0,<2.0.0)"]
-
-[[package]]
-name = "httpx"
-version = "0.23.3"
-description = "The next generation HTTP client."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-certifi = "*"
-httpcore = ">=0.15.0,<0.17.0"
-rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
-sniffio = "*"
-
-[package.extras]
-brotli = ["brotli", "brotlicffi"]
-cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
-http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (>=1.0.0,<2.0.0)"]
-
-[[package]]
-name = "identify"
-version = "2.5.24"
-description = "File identification library for Python"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-license = ["ukkonen"]
-
-[[package]]
-name = "idna"
-version = "3.4"
-description = "Internationalized Domain Names in Applications (IDNA)"
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "importlib-metadata"
-version = "6.7.0"
-description = "Read metadata from Python packages"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
-zipp = ">=0.5"
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-perf = ["ipython"]
-testing = ["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-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
-
-[[package]]
-name = "iniconfig"
-version = "2.0.0"
-description = "brain-dead simple config-ini parsing"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "jinja2"
-version = "3.1.2"
-description = "A very fast and expressive template engine."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-MarkupSafe = ">=2.0"
-
-[package.extras]
-i18n = ["Babel (>=2.7)"]
-
-[[package]]
-name = "linkify-it-py"
-version = "2.0.2"
-description = "Links recognition library with FULL unicode support."
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-uc-micro-py = "*"
-
-[package.extras]
-benchmark = ["pytest", "pytest-benchmark"]
-dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"]
-doc = ["myst-parser", "sphinx", "sphinx-book-theme"]
-test = ["coverage", "pytest", "pytest-cov"]
-
-[[package]]
-name = "markdown"
-version = "3.3.7"
-description = "Python implementation of Markdown."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
-
-[package.extras]
-testing = ["coverage", "pyyaml"]
-
-[[package]]
-name = "markdown-it-py"
-version = "2.2.0"
-description = "Python port of markdown-it. Markdown parsing, done right!"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""}
-mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""}
-mdurl = ">=0.1,<1.0"
-typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
-
-[package.extras]
-benchmarking = ["psutil", "pytest", "pytest-benchmark"]
-code-style = ["pre-commit (>=3.0,<4.0)"]
-compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
-linkify = ["linkify-it-py (>=1,<3)"]
-plugins = ["mdit-py-plugins"]
-profiling = ["gprof2dot"]
-rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
-testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
-
-[[package]]
-name = "markupsafe"
-version = "2.1.3"
-description = "Safely add untrusted strings to HTML/XML markup."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "mdit-py-plugins"
-version = "0.3.5"
-description = "Collection of plugins for markdown-it-py"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-markdown-it-py = ">=1.0.0,<3.0.0"
-
-[package.extras]
-code-style = ["pre-commit"]
-rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"]
-testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
-
-[[package]]
-name = "mdurl"
-version = "0.1.2"
-description = "Markdown URL utilities"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "mergedeep"
-version = "1.3.4"
-description = "A deep merge function for 🐍."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "mkdocs"
-version = "1.4.3"
-description = "Project documentation with Markdown."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-click = ">=7.0"
-colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
-ghp-import = ">=1.0"
-importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
-jinja2 = ">=2.11.1"
-markdown = ">=3.2.1,<3.4"
-mergedeep = ">=1.3.4"
-packaging = ">=20.5"
-pyyaml = ">=5.1"
-pyyaml-env-tag = ">=0.1"
-typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""}
-watchdog = ">=2.0"
-
-[package.extras]
-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"
-version = "0.4.1"
-description = "Automatically link across pages in MkDocs."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-Markdown = ">=3.3"
-mkdocs = ">=1.1"
-
-[[package]]
-name = "mkdocs-exclude"
-version = "1.0.2"
-description = "A mkdocs plugin that lets you exclude files or trees."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-mkdocs = "*"
-
-[[package]]
-name = "mkdocs-material"
-version = "9.1.17"
-description = "Documentation that simply works"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-colorama = ">=0.4"
-jinja2 = ">=3.0"
-markdown = ">=3.2"
-mkdocs = ">=1.4.2"
-mkdocs-material-extensions = ">=1.1"
-pygments = ">=2.14"
-pymdown-extensions = ">=9.9.1"
-regex = ">=2022.4.24"
-requests = ">=2.26"
-
-[[package]]
-name = "mkdocs-material-extensions"
-version = "1.1.1"
-description = "Extension pack for Python Markdown and MkDocs Material."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "mkdocs-rss-plugin"
-version = "1.5.0"
-description = "MkDocs plugin which generates a static RSS feed using git log and page.meta."
-category = "dev"
-optional = false
-python-versions = ">=3.7, <4"
-
-[package.dependencies]
-GitPython = ">=3.1,<3.2"
-mkdocs = ">=1.1,<2"
-pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""}
-tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""}
-
-[package.extras]
-dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"]
-doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"]
-
-[[package]]
-name = "mkdocstrings"
-version = "0.20.0"
-description = "Automatic documentation from sources, for MkDocs."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-Jinja2 = ">=2.11.1"
-Markdown = ">=3.3"
-MarkupSafe = ">=1.1"
-mkdocs = ">=1.2"
-mkdocs-autorefs = ">=0.3.1"
-mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
-pymdown-extensions = ">=6.3"
-
-[package.extras]
-crystal = ["mkdocstrings-crystal (>=0.3.4)"]
-python = ["mkdocstrings-python (>=0.5.2)"]
-python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
-
-[[package]]
-name = "mkdocstrings-python"
-version = "0.10.1"
-description = "A Python handler for mkdocstrings."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-griffe = ">=0.24"
-mkdocstrings = ">=0.20"
-
-[[package]]
-name = "msgpack"
-version = "1.0.5"
-description = "MessagePack serializer"
-category = "main"
-optional = true
-python-versions = "*"
-
-[[package]]
-name = "msgpack-types"
-version = "0.2.0"
-description = "Type stubs for msgpack"
-category = "dev"
-optional = false
-python-versions = ">=3.7,<4.0"
-
-[[package]]
-name = "multidict"
-version = "6.0.4"
-description = "multidict implementation"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "mypy"
-version = "1.4.1"
-description = "Optional static typing for Python"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-mypy-extensions = ">=1.0.0"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
-typing-extensions = ">=4.1.0"
-
-[package.extras]
-dmypy = ["psutil (>=4.0)"]
-install-types = ["pip"]
-python2 = ["typed-ast (>=1.4.0,<2)"]
-reports = ["lxml"]
-
-[[package]]
-name = "mypy-extensions"
-version = "1.0.0"
-description = "Type system extensions for programs checked with the mypy type checker."
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "nodeenv"
-version = "1.8.0"
-description = "Node.js virtual environment builder"
-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 = "23.1"
-description = "Core utilities for Python packages"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "pathspec"
-version = "0.11.1"
-description = "Utility library for gitignore style pattern matching of file paths."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "platformdirs"
-version = "3.8.0"
-description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""}
-
-[package.extras]
-docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
-test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
-
-[[package]]
-name = "pluggy"
-version = "1.2.0"
-description = "plugin and hook calling mechanisms for python"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
-
-[package.extras]
-dev = ["pre-commit", "tox"]
-testing = ["pytest", "pytest-benchmark"]
-
-[[package]]
-name = "pre-commit"
-version = "2.21.0"
-description = "A framework for managing and maintaining multi-language pre-commit hooks."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-cfgv = ">=2.0.0"
-identify = ">=1.0.0"
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
-nodeenv = ">=0.11.1"
-pyyaml = ">=5.1"
-virtualenv = ">=20.10.0"
-
-[[package]]
-name = "pygments"
-version = "2.15.1"
-description = "Pygments is a syntax highlighting package written in Python."
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-plugins = ["importlib-metadata"]
-
-[[package]]
-name = "pymdown-extensions"
-version = "10.0.1"
-description = "Extension pack for Python Markdown."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-markdown = ">=3.2"
-pyyaml = "*"
-
-[[package]]
-name = "pytest"
-version = "7.4.0"
-description = "pytest: simple powerful testing with Python"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-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"
-tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
-
-[package.extras]
-testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
-
-[[package]]
-name = "pytest-aiohttp"
-version = "1.0.4"
-description = "Pytest plugin for aiohttp support"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-aiohttp = ">=3.8.1"
-pytest = ">=6.1.0"
-pytest-asyncio = ">=0.17.2"
-
-[package.extras]
-testing = ["coverage (==6.2)", "mypy (==0.931)"]
-
-[[package]]
-name = "pytest-asyncio"
-version = "0.21.0"
-description = "Pytest support for asyncio"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-pytest = ">=7.0.0"
-typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
-
-[package.extras]
-docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.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"
-version = "2.12.1"
-description = "Pytest plugin for measuring coverage."
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[package.dependencies]
-coverage = ">=5.2.1"
-pytest = ">=4.6"
-toml = "*"
-
-[package.extras]
-testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
-
-[[package]]
-name = "pytest-textual-snapshot"
-version = "0.1.0"
-description = "Snapshot testing for Textual apps"
-category = "dev"
-optional = false
-python-versions = ">=3.6,<4.0"
-
-[package.dependencies]
-jinja2 = ">=3.0.0"
-pytest = ">=7.0.0"
-rich = ">=12.0.0"
-syrupy = ">=3.0.0"
-textual = ">=0.28.0"
-
-[[package]]
-name = "python-dateutil"
-version = "2.8.2"
-description = "Extensions to the standard Python datetime module"
-category = "dev"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
-
-[package.dependencies]
-six = ">=1.5"
-
-[[package]]
-name = "pytz"
-version = "2022.7.1"
-description = "World timezone definitions, modern and historical"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "pyyaml"
-version = "6.0"
-description = "YAML parser and emitter for Python"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "pyyaml-env-tag"
-version = "0.1"
-description = "A custom YAML tag for referencing environment variables in YAML files. "
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-pyyaml = "*"
-
-[[package]]
-name = "regex"
-version = "2023.6.3"
-description = "Alternative regular expression module, to replace re."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "requests"
-version = "2.31.0"
-description = "Python HTTP for Humans."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<4"
-idna = ">=2.5,<4"
-urllib3 = ">=1.21.1,<3"
-
-[package.extras]
-socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
-
-[[package]]
-name = "rfc3986"
-version = "1.5.0"
-description = "Validating URI References per RFC 3986"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
-
-[package.extras]
-idna2008 = ["idna"]
-
-[[package]]
-name = "rich"
-version = "13.4.2"
-description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
-category = "main"
-optional = false
-python-versions = ">=3.7.0"
-
-[package.dependencies]
-markdown-it-py = ">=2.2.0"
-pygments = ">=2.13.0,<3.0.0"
-typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""}
-
-[package.extras]
-jupyter = ["ipywidgets (>=7.5.1,<9)"]
-
-[[package]]
-name = "setuptools"
-version = "68.0.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-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "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-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "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"
-description = "Python 2 and 3 compatibility utilities"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
-
-[[package]]
-name = "smmap"
-version = "5.0.0"
-description = "A pure Python implementation of a sliding window memory map manager"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "sniffio"
-version = "1.3.0"
-description = "Sniff out which async library your code is running under"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "syrupy"
-version = "3.0.6"
-description = "Pytest Snapshot Test Utility"
-category = "dev"
-optional = false
-python-versions = ">=3.7,<4"
-
-[package.dependencies]
-colored = ">=1.3.92,<2.0.0"
-pytest = ">=5.1.0,<8.0.0"
-
-[[package]]
-name = "time-machine"
-version = "2.10.0"
-description = "Travel through time in your tests."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-python-dateutil = "*"
-
-[[package]]
-name = "toml"
-version = "0.10.2"
-description = "Python Library for Tom's Obvious, Minimal Language"
-category = "dev"
-optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-
-[[package]]
-name = "tomli"
-version = "2.0.1"
-description = "A lil' TOML parser"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "typed-ast"
-version = "1.5.4"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "types-setuptools"
-version = "67.8.0.0"
-description = "Typing stubs for setuptools"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "typing-extensions"
-version = "4.6.3"
-description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[[package]]
-name = "tzdata"
-version = "2022.7"
-description = "Provider of IANA time zone data"
-category = "dev"
-optional = false
-python-versions = ">=2"
-
-[[package]]
-name = "uc-micro-py"
-version = "1.0.2"
-description = "Micro subset of unicode data files for linkify-it-py projects."
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-test = ["coverage", "pytest", "pytest-cov"]
-
-[[package]]
-name = "urllib3"
-version = "2.0.3"
-description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
-secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
-socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["zstandard (>=0.18.0)"]
-
-[[package]]
-name = "virtualenv"
-version = "20.23.1"
-description = "Virtual Python Environment builder"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-distlib = ">=0.3.6,<1"
-filelock = ">=3.12,<4"
-importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""}
-platformdirs = ">=3.5.1,<4"
-
-[package.extras]
-docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
-test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
-
-[[package]]
-name = "watchdog"
-version = "3.0.0"
-description = "Filesystem events monitoring"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-watchmedo = ["PyYAML (>=3.10)"]
-
-[[package]]
-name = "yarl"
-version = "1.9.2"
-description = "Yet another URL library"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-idna = ">=2.0"
-multidict = ">=4.0"
-typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "zipp"
-version = "3.15.0"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "flake8 (<5)", "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"]
-
-[metadata]
-lock-version = "1.1"
-python-versions = "^3.7"
-content-hash = "54585998c7e97c8766b5daa9411b49616e014168eeeed5120390ebc19d102b15"
-
-[metadata.files]
-aiohttp = [
+files = [
{file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"},
{file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"},
{file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"},
@@ -1210,27 +96,116 @@ aiohttp = [
{file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"},
{file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"},
]
-aiosignal = [
+
+[package.dependencies]
+aiosignal = ">=1.1.2"
+async-timeout = ">=4.0.0a3,<5.0"
+asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""}
+attrs = ">=17.3.0"
+charset-normalizer = ">=2.0,<4.0"
+frozenlist = ">=1.1.1"
+multidict = ">=4.5,<7.0"
+typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
+yarl = ">=1.0,<2.0"
+
+[package.extras]
+speedups = ["Brotli", "aiodns", "cchardet"]
+
+[[package]]
+name = "aiosignal"
+version = "1.3.1"
+description = "aiosignal: a list of registered asynchronous callbacks"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
]
-anyio = [
+
+[package.dependencies]
+frozenlist = ">=1.1.0"
+
+[[package]]
+name = "anyio"
+version = "3.7.0"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0"},
{file = "anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce"},
]
-async-timeout = [
+
+[package.dependencies]
+exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
+idna = ">=2.8"
+sniffio = ">=1.1"
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+doc = ["Sphinx (>=6.1.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "sphinxcontrib-jquery"]
+test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+trio = ["trio (<0.22)"]
+
+[[package]]
+name = "async-timeout"
+version = "4.0.2"
+description = "Timeout context manager for asyncio programs"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
-asynctest = [
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "asynctest"
+version = "0.13.0"
+description = "Enhance the standard unittest package with features for testing asyncio libraries"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
]
-attrs = [
+
+[[package]]
+name = "attrs"
+version = "23.1.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
{file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
]
-black = [
+
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[docs,tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+
+[[package]]
+name = "black"
+version = "23.3.0"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
{file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
{file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
@@ -1257,19 +232,67 @@ black = [
{file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
{file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
]
-cached-property = [
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
+typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "cached-property"
+version = "1.5.2"
+description = "A decorator for caching properties in classes."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
{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 = [
+
+[[package]]
+name = "certifi"
+version = "2023.5.7"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
{file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"},
{file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"},
]
-cfgv = [
+
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+files = [
{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 = [
+
+[[package]]
+name = "charset-normalizer"
+version = "3.1.0"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "dev"
+optional = false
+python-versions = ">=3.7.0"
+files = [
{file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
{file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
{file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
@@ -1346,18 +369,54 @@ charset-normalizer = [
{file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
{file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
]
-click = [
+
+[[package]]
+name = "click"
+version = "8.1.3"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
-colorama = [
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
-colored = [
+
+[[package]]
+name = "colored"
+version = "1.4.4"
+description = "Simple library for color and formatting to terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
{file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"},
]
-coverage = [
+
+[[package]]
+name = "coverage"
+version = "7.2.7"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
{file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
{file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
@@ -1419,19 +478,61 @@ coverage = [
{file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
{file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
]
-distlib = [
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "distlib"
+version = "0.3.6"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
-exceptiongroup = [
- {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
- {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
+
+[[package]]
+name = "exceptiongroup"
+version = "1.1.2"
+description = "Backport of PEP 654 (exception groups)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"},
+ {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"},
]
-filelock = [
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "filelock"
+version = "3.12.2"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"},
{file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"},
]
-frozenlist = [
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "frozenlist"
+version = "1.3.3"
+description = "A list-like structure which implements collections.abc.MutableSequence"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"},
{file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"},
{file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"},
@@ -1507,67 +608,286 @@ frozenlist = [
{file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"},
{file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"},
]
-ghp-import = [
+
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+description = "Copy your docs directly to the gh-pages branch."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
]
-gitdb = [
+
+[package.dependencies]
+python-dateutil = ">=2.8.1"
+
+[package.extras]
+dev = ["flake8", "markdown", "twine", "wheel"]
+
+[[package]]
+name = "gitdb"
+version = "4.0.10"
+description = "Git Object Database"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"},
{file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"},
]
-gitpython = [
+
+[package.dependencies]
+smmap = ">=3.0.1,<6"
+
+[[package]]
+name = "gitpython"
+version = "3.1.31"
+description = "GitPython is a Python library used to interact with Git repositories"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"},
{file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"},
]
-griffe = [
- {file = "griffe-0.29.1-py3-none-any.whl", hash = "sha256:f9edae6b9bb2eb205bebbdd0512a162713b9342ff6e32dc596d95ff64aa71c1f"},
- {file = "griffe-0.29.1.tar.gz", hash = "sha256:460188b719e363019d0d0f4bf2d9f05cf2df24960b42a4138a1524a17b100d9b"},
+
+[package.dependencies]
+gitdb = ">=4.0.1,<5"
+typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "griffe"
+version = "0.30.1"
+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
+python-versions = ">=3.7"
+files = [
+ {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"},
+ {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"},
]
-h11 = [
+
+[package.dependencies]
+cached-property = {version = "*", markers = "python_version < \"3.8\""}
+colorama = ">=0.4"
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
-httpcore = [
+
+[package.dependencies]
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "httpcore"
+version = "0.16.3"
+description = "A minimal low-level HTTP client."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
{file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
]
-httpx = [
+
+[package.dependencies]
+anyio = ">=3.0,<5.0"
+certifi = "*"
+h11 = ">=0.13,<0.15"
+sniffio = ">=1.0.0,<2.0.0"
+
+[package.extras]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
+
+[[package]]
+name = "httpx"
+version = "0.23.3"
+description = "The next generation HTTP client."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
{file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
]
-identify = [
+
+[package.dependencies]
+certifi = "*"
+httpcore = ">=0.15.0,<0.17.0"
+rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
+sniffio = "*"
+
+[package.extras]
+brotli = ["brotli", "brotlicffi"]
+cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
+
+[[package]]
+name = "identify"
+version = "2.5.24"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"},
{file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"},
]
-idna = [
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
-importlib-metadata = [
+
+[[package]]
+name = "importlib-metadata"
+version = "6.7.0"
+description = "Read metadata from Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"},
{file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"},
]
-iniconfig = [
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+perf = ["ipython"]
+testing = ["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-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
-jinja2 = [
+
+[[package]]
+name = "jinja2"
+version = "3.1.2"
+description = "A very fast and expressive template engine."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
-linkify-it-py = [
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "linkify-it-py"
+version = "2.0.2"
+description = "Links recognition library with FULL unicode support."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"},
{file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"},
]
-markdown = [
+
+[package.dependencies]
+uc-micro-py = "*"
+
+[package.extras]
+benchmark = ["pytest", "pytest-benchmark"]
+dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"]
+doc = ["myst-parser", "sphinx", "sphinx-book-theme"]
+test = ["coverage", "pytest", "pytest-cov"]
+
+[[package]]
+name = "markdown"
+version = "3.3.7"
+description = "Python implementation of Markdown."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
{file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"},
{file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"},
]
-markdown-it-py = [
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
+
+[package.extras]
+testing = ["coverage", "pyyaml"]
+
+[[package]]
+name = "markdown-it-py"
+version = "2.2.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"},
{file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"},
]
-markupsafe = [
+
+[package.dependencies]
+linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""}
+mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""}
+mdurl = ">=0.1,<1.0"
+typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.3"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
@@ -1619,50 +939,218 @@ markupsafe = [
{file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
-mdit-py-plugins = [
+
+[[package]]
+name = "mdit-py-plugins"
+version = "0.3.5"
+description = "Collection of plugins for markdown-it-py"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"},
{file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"},
]
-mdurl = [
+
+[package.dependencies]
+markdown-it-py = ">=1.0.0,<3.0.0"
+
+[package.extras]
+code-style = ["pre-commit"]
+rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
-mergedeep = [
+
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+description = "A deep merge function for 🐍."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
-mkdocs = [
+
+[[package]]
+name = "mkdocs"
+version = "1.4.3"
+description = "Project documentation with Markdown."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "mkdocs-1.4.3-py3-none-any.whl", hash = "sha256:6ee46d309bda331aac915cd24aab882c179a933bd9e77b80ce7d2eaaa3f689dd"},
{file = "mkdocs-1.4.3.tar.gz", hash = "sha256:5955093bbd4dd2e9403c5afaf57324ad8b04f16886512a3ee6ef828956481c57"},
]
-mkdocs-autorefs = [
+
+[package.dependencies]
+click = ">=7.0"
+colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
+ghp-import = ">=1.0"
+importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
+jinja2 = ">=2.11.1"
+markdown = ">=3.2.1,<3.4"
+mergedeep = ">=1.3.4"
+packaging = ">=20.5"
+pyyaml = ">=5.1"
+pyyaml-env-tag = ">=0.1"
+typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""}
+watchdog = ">=2.0"
+
+[package.extras]
+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"
+version = "0.4.1"
+description = "Automatically link across pages in MkDocs."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{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-exclude = [
+
+[package.dependencies]
+Markdown = ">=3.3"
+mkdocs = ">=1.1"
+
+[[package]]
+name = "mkdocs-exclude"
+version = "1.0.2"
+description = "A mkdocs plugin that lets you exclude files or trees."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
{file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"},
]
-mkdocs-material = [
- {file = "mkdocs_material-9.1.17-py3-none-any.whl", hash = "sha256:809ed68427fbab0330b0b07bc93175824c3b98f4187060a5c7b46aa8ae398a75"},
- {file = "mkdocs_material-9.1.17.tar.gz", hash = "sha256:5a076524625047bf4ee4da1509ec90626f8fce915839dc07bdae6b59ff4f36f9"},
+
+[package.dependencies]
+mkdocs = "*"
+
+[[package]]
+name = "mkdocs-material"
+version = "9.1.18"
+description = "Documentation that simply works"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mkdocs_material-9.1.18-py3-none-any.whl", hash = "sha256:5bcf8fb79ac2f253c0ffe93fa181cba87718c6438f459dc4180ac7418cc9a450"},
+ {file = "mkdocs_material-9.1.18.tar.gz", hash = "sha256:981dd39979723d4cda7cfc77bbbe5e54922d5761a7af23fb8ba9edb52f114b13"},
]
-mkdocs-material-extensions = [
+
+[package.dependencies]
+colorama = ">=0.4"
+jinja2 = ">=3.0"
+markdown = ">=3.2"
+mkdocs = ">=1.4.2"
+mkdocs-material-extensions = ">=1.1"
+pygments = ">=2.14"
+pymdown-extensions = ">=9.9.1"
+regex = ">=2022.4.24"
+requests = ">=2.26"
+
+[[package]]
+name = "mkdocs-material-extensions"
+version = "1.1.1"
+description = "Extension pack for Python Markdown and MkDocs Material."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"},
{file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"},
]
-mkdocs-rss-plugin = [
+
+[[package]]
+name = "mkdocs-rss-plugin"
+version = "1.5.0"
+description = "MkDocs plugin which generates a static RSS feed using git log and page.meta."
+category = "dev"
+optional = false
+python-versions = ">=3.7, <4"
+files = [
{file = "mkdocs-rss-plugin-1.5.0.tar.gz", hash = "sha256:4178b3830dcbad9b53b12459e315b1aad6b37d1e7e5c56c686866a10f99878a4"},
{file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"},
]
-mkdocstrings = [
+
+[package.dependencies]
+GitPython = ">=3.1,<3.2"
+mkdocs = ">=1.1,<2"
+pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""}
+tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""}
+
+[package.extras]
+dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"]
+doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"]
+
+[[package]]
+name = "mkdocstrings"
+version = "0.20.0"
+description = "Automatic documentation from sources, for MkDocs."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"},
{file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"},
]
-mkdocstrings-python = [
+
+[package.dependencies]
+Jinja2 = ">=2.11.1"
+Markdown = ">=3.3"
+MarkupSafe = ">=1.1"
+mkdocs = ">=1.2"
+mkdocs-autorefs = ">=0.3.1"
+mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
+pymdown-extensions = ">=6.3"
+
+[package.extras]
+crystal = ["mkdocstrings-crystal (>=0.3.4)"]
+python = ["mkdocstrings-python (>=0.5.2)"]
+python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
+
+[[package]]
+name = "mkdocstrings-python"
+version = "0.10.1"
+description = "A Python handler for mkdocstrings."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "mkdocstrings_python-0.10.1-py3-none-any.whl", hash = "sha256:ef239cee2c688e2b949a0a47e42a141d744dd12b7007311b3309dc70e3bafc5c"},
{file = "mkdocstrings_python-0.10.1.tar.gz", hash = "sha256:b72301fff739070ec517b5b36bf2f7c49d1360a275896a64efb97fc17d3f3968"},
]
-msgpack = [
+
+[package.dependencies]
+griffe = ">=0.24"
+mkdocstrings = ">=0.20"
+
+[[package]]
+name = "msgpack"
+version = "1.0.5"
+description = "MessagePack serializer"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
{file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"},
{file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"},
{file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"},
@@ -1727,11 +1215,15 @@ msgpack = [
{file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"},
{file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"},
]
-msgpack-types = [
- {file = "msgpack-types-0.2.0.tar.gz", hash = "sha256:b6b7ce9f52599f9dc3497006be8cf6bed7bd2c83fa48c4df43ac6958b97b0720"},
- {file = "msgpack_types-0.2.0-py3-none-any.whl", hash = "sha256:7e5bce9e3bba9fe08ed14005ad107aa44ea8d4b779ec28b8db880826d4c67303"},
-]
-multidict = [
+
+[[package]]
+name = "multidict"
+version = "6.0.4"
+description = "multidict implementation"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"},
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"},
{file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"},
@@ -1807,7 +1299,15 @@ multidict = [
{file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"},
{file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
]
-mypy = [
+
+[[package]]
+name = "mypy"
+version = "1.4.1"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"},
{file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"},
{file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"},
@@ -1835,71 +1335,297 @@ mypy = [
{file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"},
{file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"},
]
-mypy-extensions = [
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
-nodeenv = [
+
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
]
-packaging = [
+
+[package.dependencies]
+setuptools = "*"
+
+[[package]]
+name = "packaging"
+version = "23.1"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
]
-pathspec = [
+
+[[package]]
+name = "pathspec"
+version = "0.11.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
]
-platformdirs = [
+
+[[package]]
+name = "platformdirs"
+version = "3.8.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"},
{file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"},
]
-pluggy = [
+
+[package.dependencies]
+typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
+
+[[package]]
+name = "pluggy"
+version = "1.2.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
{file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
]
-pre-commit = [
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pre-commit"
+version = "2.21.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"},
{file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"},
]
-pygments = [
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "pygments"
+version = "2.15.1"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"},
{file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"},
]
-pymdown-extensions = [
+
+[package.extras]
+plugins = ["importlib-metadata"]
+
+[[package]]
+name = "pymdown-extensions"
+version = "10.0.1"
+description = "Extension pack for Python Markdown."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "pymdown_extensions-10.0.1-py3-none-any.whl", hash = "sha256:ae66d84013c5d027ce055693e09a4628b67e9dec5bce05727e45b0918e36f274"},
{file = "pymdown_extensions-10.0.1.tar.gz", hash = "sha256:b44e1093a43b8a975eae17b03c3a77aad4681b3b56fce60ce746dbef1944c8cb"},
]
-pytest = [
+
+[package.dependencies]
+markdown = ">=3.2"
+pyyaml = "*"
+
+[[package]]
+name = "pytest"
+version = "7.4.0"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"},
{file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"},
]
-pytest-aiohttp = [
+
+[package.dependencies]
+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"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-aiohttp"
+version = "1.0.4"
+description = "Pytest plugin for aiohttp support"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{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 = [
+
+[package.dependencies]
+aiohttp = ">=3.8.1"
+pytest = ">=6.1.0"
+pytest-asyncio = ">=0.17.2"
+
+[package.extras]
+testing = ["coverage (==6.2)", "mypy (==0.931)"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.21.0"
+description = "Pytest support for asyncio"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "pytest-asyncio-0.21.0.tar.gz", hash = "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b"},
{file = "pytest_asyncio-0.21.0-py3-none-any.whl", hash = "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c"},
]
-pytest-cov = [
+
+[package.dependencies]
+pytest = ">=7.0.0"
+typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.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"
+version = "2.12.1"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
{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"},
]
-pytest-textual-snapshot = [
+
+[package.dependencies]
+coverage = ">=5.2.1"
+pytest = ">=4.6"
+toml = "*"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "pytest-textual-snapshot"
+version = "0.1.0"
+description = "Snapshot testing for Textual apps"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+files = [
{file = "pytest_textual_snapshot-0.1.0-py3-none-any.whl", hash = "sha256:7310002ed152ce6cc654fff7f83ec88eecc36116c5bf7995decc0a6424809da3"},
{file = "pytest_textual_snapshot-0.1.0.tar.gz", hash = "sha256:5e20629f2413a3689a485117e709e6d4010b7b5b558c0070414899b9768697f0"},
]
-python-dateutil = [
+
+[package.dependencies]
+jinja2 = ">=3.0.0"
+pytest = ">=7.0.0"
+rich = ">=12.0.0"
+syrupy = ">=3.0.0"
+textual = ">=0.28.0"
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
-pytz = [
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pytz"
+version = "2022.7.1"
+description = "World timezone definitions, modern and historical"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
{file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"},
{file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
]
-pyyaml = [
+
+[[package]]
+name = "pyyaml"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
@@ -1941,11 +1667,30 @@ pyyaml = [
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
-pyyaml-env-tag = [
+
+[[package]]
+name = "pyyaml-env-tag"
+version = "0.1"
+description = "A custom YAML tag for referencing environment variables in YAML files. "
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
-regex = [
+
+[package.dependencies]
+pyyaml = "*"
+
+[[package]]
+name = "regex"
+version = "2023.6.3"
+description = "Alternative regular expression module, to replace re."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
{file = "regex-2023.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:824bf3ac11001849aec3fa1d69abcb67aac3e150a933963fb12bda5151fe1bfd"},
{file = "regex-2023.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05ed27acdf4465c95826962528f9e8d41dbf9b1aa8531a387dee6ed215a3e9ef"},
{file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b49c764f88a79160fa64f9a7b425620e87c9f46095ef9c9920542ab2495c8bc"},
@@ -2035,39 +1780,163 @@ regex = [
{file = "regex-2023.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:bdff5eab10e59cf26bc479f565e25ed71a7d041d1ded04ccf9aee1d9f208487a"},
{file = "regex-2023.6.3.tar.gz", hash = "sha256:72d1a25bf36d2050ceb35b517afe13864865268dfb45910e2e17a84be6cbfeb0"},
]
-requests = [
+
+[[package]]
+name = "requests"
+version = "2.31.0"
+description = "Python HTTP for Humans."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
]
-rfc3986 = [
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "rfc3986"
+version = "1.5.0"
+description = "Validating URI References per RFC 3986"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
-rich = [
+
+[package.dependencies]
+idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
+
+[package.extras]
+idna2008 = ["idna"]
+
+[[package]]
+name = "rich"
+version = "13.4.2"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+files = [
{file = "rich-13.4.2-py3-none-any.whl", hash = "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec"},
{file = "rich-13.4.2.tar.gz", hash = "sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898"},
]
-setuptools = [
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""}
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "setuptools"
+version = "68.0.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
{file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
]
-six = [
+
+[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-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "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-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "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"
+description = "Python 2 and 3 compatibility utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
-smmap = [
+
+[[package]]
+name = "smmap"
+version = "5.0.0"
+description = "A pure Python implementation of a sliding window memory map manager"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
{file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
{file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
]
-sniffio = [
+
+[[package]]
+name = "sniffio"
+version = "1.3.0"
+description = "Sniff out which async library your code is running under"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
-syrupy = [
+
+[[package]]
+name = "syrupy"
+version = "3.0.6"
+description = "Pytest Snapshot Test Utility"
+category = "dev"
+optional = false
+python-versions = ">=3.7,<4"
+files = [
{file = "syrupy-3.0.6-py3-none-any.whl", hash = "sha256:9c18e22264026b34239bcc87ab7cc8d893eb17236ea7dae634217ea4f22a848d"},
{file = "syrupy-3.0.6.tar.gz", hash = "sha256:583aa5ca691305c27902c3e29a1ce9da50ff9ab5f184c54b1dc124a16e4a6cf4"},
]
-time-machine = [
+
+[package.dependencies]
+colored = ">=1.3.92,<2.0.0"
+pytest = ">=5.1.0,<8.0.0"
+
+[[package]]
+name = "textual-dev"
+version = "1.0.0"
+description = "Development tools for working with Textual"
+category = "dev"
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "textual_dev-1.0.0-py3-none-any.whl", hash = "sha256:af6ca102bd46cf5115a108329cdfe1c7aec26cab81e83fd78d884648ad84e23b"},
+ {file = "textual_dev-1.0.0.tar.gz", hash = "sha256:1aa32f58976eb63078d2b8454e2d40a7806d7b205e3ee89e34ca2941548dbbd8"},
+]
+
+[package.dependencies]
+aiohttp = ">=3.8.1"
+click = ">=8.1.2"
+msgpack = ">=1.0.3"
+textual = "*"
+typing-extensions = ">=4.4.0,<5.0.0"
+
+[[package]]
+name = "time-machine"
+version = "2.10.0"
+description = "Travel through time in your tests."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d5e93c14b935d802a310c1d4694a9fe894b48a733ebd641c9a570d6f9e1f667"},
{file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c0dda6b132c0180941944ede357109016d161d840384c2fb1096a3a2ef619f4"},
{file = "time_machine-2.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:900517e4a4121bf88527343d6aea2b5c99df134815bb8271ef589ec792502a71"},
@@ -2123,15 +1992,42 @@ time-machine = [
{file = "time_machine-2.10.0-cp39-cp39-win_arm64.whl", hash = "sha256:c1775a949dd830579d1af5a271ec53d920dc01657035ad305f55c5a1ac9b9f1e"},
{file = "time_machine-2.10.0.tar.gz", hash = "sha256:64fd89678cf589fc5554c311417128b2782222dd65f703bf248ef41541761da0"},
]
-toml = [
+
+[package.dependencies]
+python-dateutil = "*"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
-tomli = [
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
-typed-ast = [
+
+[[package]]
+name = "typed-ast"
+version = "1.5.4"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
@@ -2157,31 +2053,106 @@ 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"},
]
-types-setuptools = [
+
+[[package]]
+name = "types-setuptools"
+version = "67.8.0.0"
+description = "Typing stubs for setuptools"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
{file = "types-setuptools-67.8.0.0.tar.gz", hash = "sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f"},
{file = "types_setuptools-67.8.0.0-py3-none-any.whl", hash = "sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff"},
]
-typing-extensions = [
- {file = "typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26"},
- {file = "typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"},
+
+[[package]]
+name = "typing-extensions"
+version = "4.7.1"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
+ {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
]
-tzdata = [
+
+[[package]]
+name = "tzdata"
+version = "2022.7"
+description = "Provider of IANA time zone data"
+category = "dev"
+optional = false
+python-versions = ">=2"
+files = [
{file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"},
{file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"},
]
-uc-micro-py = [
+
+[[package]]
+name = "uc-micro-py"
+version = "1.0.2"
+description = "Micro subset of unicode data files for linkify-it-py projects."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"},
{file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"},
]
-urllib3 = [
+
+[package.extras]
+test = ["coverage", "pytest", "pytest-cov"]
+
+[[package]]
+name = "urllib3"
+version = "2.0.3"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"},
{file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"},
]
-virtualenv = [
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.23.1"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "virtualenv-20.23.1-py3-none-any.whl", hash = "sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419"},
{file = "virtualenv-20.23.1.tar.gz", hash = "sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1"},
]
-watchdog = [
+
+[package.dependencies]
+distlib = ">=0.3.6,<1"
+filelock = ">=3.12,<4"
+importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""}
+platformdirs = ">=3.5.1,<4"
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
+
+[[package]]
+name = "watchdog"
+version = "3.0.0"
+description = "Filesystem events monitoring"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"},
{file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"},
{file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"},
@@ -2210,7 +2181,18 @@ watchdog = [
{file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"},
{file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"},
]
-yarl = [
+
+[package.extras]
+watchmedo = ["PyYAML (>=3.10)"]
+
+[[package]]
+name = "yarl"
+version = "1.9.2"
+description = "Yet another URL library"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"},
{file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"},
{file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"},
@@ -2286,7 +2268,29 @@ yarl = [
{file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"},
{file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"},
]
-zipp = [
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "zipp"
+version = "3.15.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
{file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
{file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "flake8 (<5)", "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)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.7"
+content-hash = "652f05ae0b8df5f39af720c34bfb222e514e1b17afbf0eb10738946651c293e1"
diff --git a/pyproject.toml b/pyproject.toml
index 25285a195..6ebfc2cd3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
-version = "0.28.1"
+version = "0.29.0"
homepage = "https://github.com/Textualize/textual"
description = "Modern Text User Interface framework"
authors = ["Will McGugan "]
@@ -33,9 +33,6 @@ include = [
{ path = "docs-offline/**/*", format = "sdist" }
]
-[tool.poetry.scripts]
-textual = "textual.cli.cli:run"
-
[tool.poetry.dependencies]
python = "^3.7"
rich = ">=13.3.3"
@@ -44,14 +41,6 @@ markdown-it-py = {extras = ["plugins", "linkify"], version = ">=2.1.0"}
importlib-metadata = ">=4.11.3"
typing-extensions = "^4.4.0"
-# 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 }
-
-[tool.poetry.extras]
-dev = ["aiohttp", "click", "msgpack"]
-
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.3"
black = "^23.1.0"
@@ -67,8 +56,9 @@ pytest-aiohttp = "^1.0.4"
time-machine = "^2.6.0"
mkdocs-rss-plugin = "^1.5.0"
httpx = "^0.23.1"
-msgpack-types = "^0.2.0"
types-setuptools = "^67.2.0.1"
+textual-dev = ">=1.0.0"
+pytest-asyncio = "*"
pytest-textual-snapshot = ">=0.1.0"
[tool.black]
diff --git a/questions/compose-result.question.md b/questions/compose-result.question.md
index d148f256a..bd18b5ce6 100644
--- a/questions/compose-result.question.md
+++ b/questions/compose-result.question.md
@@ -10,5 +10,5 @@ You likely have an older version of Textual. You can install the latest version
The following should do it:
```
-pip install "textual[dev]" -U
+pip install textual-dev -U
```
diff --git a/src/textual/app.py b/src/textual/app.py
index 322dbe941..fb08198fb 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -96,11 +96,11 @@ from .screen import Screen, ScreenResultCallbackType, ScreenResultType
from .widget import AwaitMount, Widget
if TYPE_CHECKING:
+ from textual_dev.client import DevtoolsClient
from typing_extensions import Coroutine, TypeAlias
# Unused & ignored imports are needed for the docs to link to these objects:
from .css.query import WrongType # type: ignore # noqa: F401
- from .devtools.client import DevtoolsClient
from .message import Message
from .pilot import Pilot
from .widget import MountError # type: ignore # noqa: F401
@@ -432,7 +432,7 @@ class App(Generic[ReturnType], DOMNode):
self.devtools: DevtoolsClient | None = None
if "devtools" in self.features:
try:
- from .devtools.client import DevtoolsClient
+ from textual_dev.client import DevtoolsClient
except ImportError:
# Dev dependencies not installed
pass
@@ -806,7 +806,7 @@ class App(Generic[ReturnType], DOMNode):
return
try:
- from .devtools.client import DevtoolsLog
+ from textual_dev.client import DevtoolsLog
if len(objects) == 1 and not kwargs:
devtools.log(
@@ -1859,7 +1859,7 @@ class App(Generic[ReturnType], DOMNode):
active_message_pump.set(self)
if self.devtools is not None:
- from .devtools.client import DevtoolsConnectionError
+ from textual_dev.client import DevtoolsConnectionError
try:
await self.devtools.connect()
@@ -1971,7 +1971,7 @@ class App(Generic[ReturnType], DOMNode):
if self.devtools is not None:
devtools = self.devtools
assert devtools is not None
- from .devtools.redirect_output import StdoutRedirector
+ from textual_dev.redirect_output import StdoutRedirector
redirector = StdoutRedirector(devtools)
with redirect_stderr(redirector):
diff --git a/src/textual/cli/__init__.py b/src/textual/cli/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/textual/cli/__main__.py b/src/textual/cli/__main__.py
deleted file mode 100644
index 27cfa4889..000000000
--- a/src/textual/cli/__main__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .cli import run
-
-run()
diff --git a/src/textual/cli/_run.py b/src/textual/cli/_run.py
deleted file mode 100644
index 694d5b34d..000000000
--- a/src/textual/cli/_run.py
+++ /dev/null
@@ -1,148 +0,0 @@
-"""
-
-Functions to run Textual apps with an updated environment.
-
-Note that these methods will execute apps in a new process, and abandon the current process.
-This means that (if they succeed) they will never return.
-"""
-
-from __future__ import annotations
-
-import os
-import platform
-import subprocess
-import sys
-from string import Template
-from typing import NoReturn, Sequence
-
-WINDOWS = platform.system() == "Windows"
-
-EXEC_SCRIPT = Template(
- """\
-from textual.app import App
-try:
- from $MODULE import $APP as app;
-except ImportError:
- raise SystemExit("Unable to import '$APP' from module '$MODULE'") from None
-
-if isinstance(app, App):
- # If we imported an app, run it
- app.run()
-else:
- # Otherwise it is assumed to be a class
- app().run()
-"""
-)
-"""A template script to import and run an app."""
-
-
-class ExecImportError(Exception):
- """Raised if a Python import is invalid."""
-
-
-def run_app(
- import_name: str, args: Sequence[str], environment: dict[str, str] | None = None
-) -> None:
- """Run a textual app.
-
- Note:
- The current process is abandoned.
-
- Args:
- command_args: Arguments to pass to the Textual app.
- environment: Environment variables, or None to use current process.
- """
- if environment is None:
- app_environment = dict(os.environ)
- else:
- app_environment = environment
-
- if _is_python_path(import_name):
- # If it is a Python path we can exec it now
- exec_python([import_name, *args], app_environment)
-
- else:
- # Otherwise it is assumed to be a Python import
- try:
- exec_import(import_name, args, app_environment)
- except (SyntaxError, ExecImportError):
- print(f"Unable to import Textual app from {import_name!r}")
-
-
-def _is_python_path(path: str) -> bool:
- """Does the given file look like it's run with Python?
-
- Args:
- path: The path to check.
-
- Returns:
- True if the path references Python code, False it it doesn't.
- """
- if path.endswith(".py"):
- return True
- try:
- with open(path, "r") as source:
- first_line = source.readline()
- except IOError:
- return False
- return first_line.startswith("#!") and "py" in first_line
-
-
-def _flush() -> None:
- """Flush output before exec."""
- sys.stderr.flush()
- sys.stdout.flush()
-
-
-def exec_python(args: Sequence[str], environment: dict[str, str]) -> None:
- """Execute a Python script.
-
- Args:
- args: Additional arguments.
- environment: Environment variables.
- """
- _flush()
- if WINDOWS:
- subprocess.call([sys.executable, *args], env=environment)
- else:
- os.execvpe(sys.executable, ["python", *args], environment)
-
-
-def exec_command(
- command: str, args: Sequence[str], environment: dict[str, str]
-) -> None:
- """Execute a command with the given environment.
-
- Args:
- command: Command to execute.
- environment: Environment variables.
- """
- _flush()
- if WINDOWS:
- subprocess.call([command, *args], env=environment)
- else:
- os.execvpe(command, [command, *args], environment)
-
-
-def exec_import(
- import_name: str, args: Sequence[str], environment: dict[str, str]
-) -> None:
- """Import and execute an app.
-
- Raises:
- SyntaxError: If any imports are not valid Python symbols.
- ExecImportError: If the module could not be imported.
-
- Args:
- import_name: The Python import.
- args: Command line arguments.
- environment: Environment variables.
- """
- module, _colon, app = import_name.partition(":")
- app = app or "app"
-
- script = EXEC_SCRIPT.substitute(MODULE=module, APP=app)
- # Compiling the script will raise a SyntaxError if there are any invalid symbols
- compile(script, "textual-exec", "exec")
- _flush()
- exec_python(["-c", script, *args], environment)
diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py
deleted file mode 100644
index 0eb6376d3..000000000
--- a/src/textual/cli/cli.py
+++ /dev/null
@@ -1,241 +0,0 @@
-from __future__ import annotations
-
-import platform
-import shlex
-import sys
-
-from ..constants import DEVTOOLS_PORT
-from ._run import exec_command, run_app
-
-try:
- import click
-except ImportError:
- print("Please install 'textual[dev]' to use the 'textual' command")
- sys.exit(1)
-
-from importlib_metadata import version
-
-WINDOWS = platform.system() == "Windows"
-"""True if we're running on Windows."""
-
-
-@click.group()
-@click.version_option(version("textual"))
-def run():
- pass
-
-
-@run.command(help="Run the Textual Devtools console.")
-@click.option(
- "--port",
- "port",
- type=int,
- default=None,
- metavar="PORT",
- help=f"Port to use for the development mode console. Defaults to {DEVTOOLS_PORT}.",
-)
-@click.option("-v", "verbose", help="Enable verbose logs.", is_flag=True)
-@click.option("-x", "--exclude", "exclude", help="Exclude log group(s)", multiple=True)
-def console(port: int | None, verbose: bool, exclude: list[str]) -> None:
- """Launch the textual console."""
-
- from rich.console import Console
-
- from textual.devtools.server import _run_devtools
-
- console = Console()
- console.clear()
- console.show_cursor(False)
- try:
- _run_devtools(verbose=verbose, exclude=exclude, port=port)
- finally:
- console.show_cursor(True)
-
-
-def _pre_run_warnings() -> None:
- """Look for and report any issues with the environment.
-
- This is the right place to add code that looks at the terminal, or other
- environmental issues, and if a problem is seen it should be printed so
- the developer can see it easily.
- """
- import os
-
- from rich.console import Console
- from rich.panel import Panel
-
- console = Console()
-
- # Add any test/warning pair here. The list contains a tuple where the
- # first item is `True` if a problem situation is detected, and the
- # second item is a message to show the user on exit from `textual run`.
- warnings = [
- (
- (
- platform.system() == "Darwin"
- and os.environ.get("TERM_PROGRAM") == "Apple_Terminal"
- ),
- "The default terminal app on macOS is limited to 256 colors. See our FAQ for more details:\n\n"
- "https://github.com/Textualize/textual/blob/main/FAQ.md#why-doesn't-textual-look-good-on-macos",
- )
- ]
-
- for concerning, concern in warnings:
- if concerning:
- console.print(
- Panel.fit(
- f"⚠️ {concern}", style="yellow", border_style="red", padding=(1, 2)
- )
- )
-
-
-@run.command(
- "run",
- context_settings={
- "ignore_unknown_options": True,
- },
-)
-@click.argument("import_name", metavar="FILE or FILE:APP")
-@click.option("--dev", "dev", help="Enable development mode.", is_flag=True)
-@click.option(
- "--port",
- "port",
- type=int,
- default=None,
- metavar="PORT",
- help=f"Port to use for the development mode console. Defaults to {DEVTOOLS_PORT}.",
-)
-@click.option(
- "--press", "press", default=None, help="Comma separated keys to simulate press."
-)
-@click.option(
- "--screenshot",
- type=int,
- default=None,
- metavar="DELAY",
- help="Take screenshot after DELAY seconds.",
-)
-@click.option(
- "-c",
- "--command",
- "command",
- type=bool,
- default=False,
- help="Run as command rather that a file / module.",
- is_flag=True,
-)
-@click.option(
- "-r",
- "--show-return",
- "show_return",
- type=bool,
- default=False,
- help="Show any return value on exit.",
- is_flag=True,
-)
-@click.argument("extra_args", nargs=-1, type=click.UNPROCESSED)
-def _run_app(
- import_name: str,
- dev: bool,
- port: int | None,
- press: str | None,
- screenshot: int | None,
- extra_args: tuple[str],
- command: bool = False,
- show_return: bool = False,
-) -> None:
- """Run a Textual app.
-
- The app to run may be given as a path (ending with .py) which will be equivalent to running the
- script with python, or as a Python import which will import and run an app called "app".
-
- In the case of an import, you can import and run an alternative app by appending a colon followed
- by the name of the app instance or class.
-
- Here are some examples:
-
- textual run foo.py
-
- textual run module.foo
-
- textual run module.foo:MyApp
-
- Add the --dev switch to enable the textual console.
-
- textual run --dev foo.py
-
- Use the -c switch to run a command that launches a Textual app.
-
- textual run -c textual colors
-
- """
-
- import os
-
- from textual.features import parse_features
-
- environment = dict(os.environ)
-
- features = set(parse_features(environment.get("TEXTUAL", "")))
- if dev:
- features.add("debug")
- features.add("devtools")
-
- environment["TEXTUAL"] = ",".join(sorted(features))
- if port is not None:
- environment["TEXTUAL_DEVTOOLS_PORT"] = str(port)
- if press is not None:
- environment["TEXTUAL_PRESS"] = str(press)
- if screenshot is not None:
- environment["TEXTUAL_SCREENSHOT"] = str(screenshot)
- if show_return:
- environment["TEXTUAL_SHOW_RETURN"] = "1"
-
- _pre_run_warnings()
-
- import_name, *args = [*shlex.split(import_name, posix=not WINDOWS), *extra_args]
-
- if command:
- exec_command(import_name, args, environment)
- else:
- run_app(import_name, args, environment)
-
-
-@run.command("borders")
-def borders():
- """Explore the border styles available in Textual."""
- from textual.cli.previews import borders
-
- borders.app.run()
-
-
-@run.command("easing")
-def easing():
- """Explore the animation easing functions available in Textual."""
- from textual.cli.previews import easing
-
- easing.app.run()
-
-
-@run.command("colors")
-def colors():
- """Explore the design system."""
- from textual.cli.previews import colors
-
- colors.app.run()
-
-
-@run.command("keys")
-def keys():
- """Show key events."""
- from textual.cli.previews import keys
-
- keys.app.run()
-
-
-@run.command("diagnose")
-def run_diagnose():
- """Print information about the Textual environment."""
- from textual.cli.tools.diagnose import diagnose
-
- diagnose()
diff --git a/src/textual/cli/previews/__init__.py b/src/textual/cli/previews/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/textual/cli/previews/borders.py b/src/textual/cli/previews/borders.py
deleted file mode 100644
index 923ad9cc5..000000000
--- a/src/textual/cli/previews/borders.py
+++ /dev/null
@@ -1,71 +0,0 @@
-from textual.app import App, ComposeResult
-from textual.containers import Vertical
-from textual.css.constants import VALID_BORDER
-from textual.widgets import Button, Label
-
-TEXT = """I must not fear.
-Fear is the mind-killer.
-Fear is the little-death that brings total obliteration.
-I will face my fear.
-I will permit it to pass over me and through me.
-And when it has gone past, I will turn the inner eye to see its path.
-Where the fear has gone there will be nothing. Only I will remain."""
-
-
-class BorderButtons(Vertical):
- DEFAULT_CSS = """
- BorderButtons {
- dock: left;
- width: 24;
- overflow-y: scroll;
- }
-
- BorderButtons > Button {
- width: 100%;
- }
- """
-
- def compose(self) -> ComposeResult:
- for border in sorted(VALID_BORDER):
- if border:
- yield Button(border, id=border)
-
-
-class BorderApp(App):
- """Demonstrates the border styles."""
-
- CSS = """
- Screen {
- align: center middle;
- overflow: auto;
- }
- #text {
- margin: 2 4;
- padding: 2 4;
- border: solid $secondary;
- height: auto;
- background: $panel;
- color: $text;
- border-title-align: center;
- }
- """
-
- def compose(self):
- yield BorderButtons()
- self.text = Label(TEXT, id="text")
- self.text.shrink = True
- self.text.border_title = "solid"
- yield self.text
-
- def on_button_pressed(self, event: Button.Pressed) -> None:
- self.text.border_title = event.button.id
- self.text.styles.border = (
- event.button.id,
- self.stylesheet._variables["secondary"],
- )
-
-
-app = BorderApp()
-
-if __name__ == "__main__":
- app.run()
diff --git a/src/textual/cli/previews/colors.css b/src/textual/cli/previews/colors.css
deleted file mode 100644
index 38a6ce045..000000000
--- a/src/textual/cli/previews/colors.css
+++ /dev/null
@@ -1,332 +0,0 @@
-Label {
- width: 100%;
-}
-
-ColorButtons {
- dock: left;
- overflow-y: auto;
- width: 30;
-}
-
-ColorButtons > Button {
- width: 100%;
-}
-
-ColorsView {
- width: 100%;
- height: 100%;
- align: center middle;
- overflow-x: auto;
- background: $background;
- scrollbar-gutter: stable;
-}
-
-ColorItem {
- layout: horizontal;
- height: 3;
- width: 1fr;
-}
-
-ColorBar {
- height: auto;
- width: 1fr;
- content-align: center middle;
-}
-
-ColorBar.label {
- width: 2fr;
-
-}
-
-ColorItem {
- width: 100%;
- padding: 1 2;
-}
-
-ColorGroup {
- margin: 2 0;
- width: 80;
- height: auto;
- padding: 1 4 2 4;
- background: $surface;
- border: wide $surface;
-}
-
-
-ColorGroup.-active {
- border: wide $secondary;
-}
-
-.text {
- color: $text;
-}
-
-.muted {
- color: $text-muted;
-}
-
-
-.disabled {
- color: $text-disabled;
-}
-
-
-Label {
- padding: 0 0 1 0;
- content-align: center middle;
- color: $text;
- text-style: bold;
-}
-
-.primary-darken-3 {
- background: $primary-darken-3;
-}
-.primary-darken-2 {
- background: $primary-darken-2;
-}
-.primary-darken-1 {
- background: $primary-darken-1;
-}
-.primary {
- background: $primary;
-}
-.primary-lighten-1 {
- background: $primary-lighten-1;
-}
-.primary-lighten-2 {
- background: $primary-lighten-2;
-}
-.primary-lighten-3 {
- background: $primary-lighten-3;
-}
-.secondary-darken-3 {
- background: $secondary-darken-3;
-}
-.secondary-darken-2 {
- background: $secondary-darken-2;
-}
-.secondary-darken-1 {
- background: $secondary-darken-1;
-}
-.secondary {
- background: $secondary;
-}
-.secondary-lighten-1 {
- background: $secondary-lighten-1;
-}
-.secondary-lighten-2 {
- background: $secondary-lighten-2;
-}
-.secondary-lighten-3 {
- background: $secondary-lighten-3;
-}
-.background-darken-3 {
- background: $background-darken-3;
-}
-.background-darken-2 {
- background: $background-darken-2;
-}
-.background-darken-1 {
- background: $background-darken-1;
-}
-.background {
- background: $background;
-}
-.background-lighten-1 {
- background: $background-lighten-1;
-}
-.background-lighten-2 {
- background: $background-lighten-2;
-}
-.background-lighten-3 {
- background: $background-lighten-3;
-}
-.primary-background-darken-3 {
- background: $primary-background-darken-3;
-}
-.primary-background-darken-2 {
- background: $primary-background-darken-2;
-}
-.primary-background-darken-1 {
- background: $primary-background-darken-1;
-}
-.primary-background {
- background: $primary-background;
-}
-.primary-background-lighten-1 {
- background: $primary-background-lighten-1;
-}
-.primary-background-lighten-2 {
- background: $primary-background-lighten-2;
-}
-.primary-background-lighten-3 {
- background: $primary-background-lighten-3;
-}
-.secondary-background-darken-3 {
- background: $secondary-background-darken-3;
-}
-.secondary-background-darken-2 {
- background: $secondary-background-darken-2;
-}
-.secondary-background-darken-1 {
- background: $secondary-background-darken-1;
-}
-.secondary-background {
- background: $secondary-background;
-}
-.secondary-background-lighten-1 {
- background: $secondary-background-lighten-1;
-}
-.secondary-background-lighten-2 {
- background: $secondary-background-lighten-2;
-}
-.secondary-background-lighten-3 {
- background: $secondary-background-lighten-3;
-}
-.surface-darken-3 {
- background: $surface-darken-3;
-}
-.surface-darken-2 {
- background: $surface-darken-2;
-}
-.surface-darken-1 {
- background: $surface-darken-1;
-}
-.surface {
- background: $surface;
-}
-.surface-lighten-1 {
- background: $surface-lighten-1;
-}
-.surface-lighten-2 {
- background: $surface-lighten-2;
-}
-.surface-lighten-3 {
- background: $surface-lighten-3;
-}
-.panel-darken-3 {
- background: $panel-darken-3;
-}
-.panel-darken-2 {
- background: $panel-darken-2;
-}
-.panel-darken-1 {
- background: $panel-darken-1;
-}
-.panel {
- background: $panel;
-}
-.panel-lighten-1 {
- background: $panel-lighten-1;
-}
-.panel-lighten-2 {
- background: $panel-lighten-2;
-}
-.panel-lighten-3 {
- background: $panel-lighten-3;
-}
-.boost-darken-3 {
- background: $boost-darken-3;
-}
-.boost-darken-2 {
- background: $boost-darken-2;
-}
-.boost-darken-1 {
- background: $boost-darken-1;
-}
-.boost {
- background: $boost;
-}
-.boost-lighten-1 {
- background: $boost-lighten-1;
-}
-.boost-lighten-2 {
- background: $boost-lighten-2;
-}
-.boost-lighten-3 {
- background: $boost-lighten-3;
-}
-.warning-darken-3 {
- background: $warning-darken-3;
-}
-.warning-darken-2 {
- background: $warning-darken-2;
-}
-.warning-darken-1 {
- background: $warning-darken-1;
-}
-.warning {
- background: $warning;
-}
-.warning-lighten-1 {
- background: $warning-lighten-1;
-}
-.warning-lighten-2 {
- background: $warning-lighten-2;
-}
-.warning-lighten-3 {
- background: $warning-lighten-3;
-}
-.error-darken-3 {
- background: $error-darken-3;
-}
-.error-darken-2 {
- background: $error-darken-2;
-}
-.error-darken-1 {
- background: $error-darken-1;
-}
-.error {
- background: $error;
-}
-.error-lighten-1 {
- background: $error-lighten-1;
-}
-.error-lighten-2 {
- background: $error-lighten-2;
-}
-.error-lighten-3 {
- background: $error-lighten-3;
-}
-.success-darken-3 {
- background: $success-darken-3;
-}
-.success-darken-2 {
- background: $success-darken-2;
-}
-.success-darken-1 {
- background: $success-darken-1;
-}
-.success {
- background: $success;
-}
-.success-lighten-1 {
- background: $success-lighten-1;
-}
-.success-lighten-2 {
- background: $success-lighten-2;
-}
-.success-lighten-3 {
- background: $success-lighten-3;
-}
-.accent-darken-3 {
- background: $accent-darken-3;
-}
-.accent-darken-2 {
- background: $accent-darken-2;
-}
-.accent-darken-1 {
- background: $accent-darken-1;
-}
-.accent {
- background: $accent;
-}
-.accent-lighten-1 {
- background: $accent-lighten-1;
-}
-.accent-lighten-2 {
- background: $accent-lighten-2;
-}
-.accent-lighten-3 {
- background: $accent-lighten-3;
-}
diff --git a/src/textual/cli/previews/colors.py b/src/textual/cli/previews/colors.py
deleted file mode 100644
index b069251e6..000000000
--- a/src/textual/cli/previews/colors.py
+++ /dev/null
@@ -1,79 +0,0 @@
-from textual.app import App, ComposeResult
-from textual.containers import Horizontal, Vertical, VerticalScroll
-from textual.design import ColorSystem
-from textual.widgets import Button, Footer, Label, Static
-
-
-class ColorButtons(VerticalScroll):
- def compose(self) -> ComposeResult:
- for border in ColorSystem.COLOR_NAMES:
- if border:
- yield Button(border, id=border)
-
-
-class ColorBar(Static):
- pass
-
-
-class ColorItem(Horizontal):
- pass
-
-
-class ColorGroup(Vertical):
- pass
-
-
-class Content(Vertical):
- pass
-
-
-class ColorsView(VerticalScroll):
- def compose(self) -> ComposeResult:
- LEVELS = [
- "darken-3",
- "darken-2",
- "darken-1",
- "",
- "lighten-1",
- "lighten-2",
- "lighten-3",
- ]
-
- for color_name in ColorSystem.COLOR_NAMES:
- with ColorGroup(id=f"group-{color_name}"):
- yield Label(f'"{color_name}"')
- for level in LEVELS:
- color = f"{color_name}-{level}" if level else color_name
- with ColorItem(classes=color):
- yield ColorBar(f"${color}", classes="text label")
- yield ColorBar("$text-muted", classes="muted")
- yield ColorBar("$text-disabled", classes="disabled")
-
-
-class ColorsApp(App):
- CSS_PATH = "colors.css"
-
- BINDINGS = [("d", "toggle_dark", "Toggle dark mode")]
-
- def compose(self) -> ComposeResult:
- yield Content(ColorButtons())
- yield Footer()
-
- def on_mount(self) -> None:
- self.call_after_refresh(self.update_view)
-
- def update_view(self) -> None:
- content = self.query_one("Content", Content)
- content.mount(ColorsView())
-
- def on_button_pressed(self, event: Button.Pressed) -> None:
- self.query(ColorGroup).remove_class("-active")
- group = self.query_one(f"#group-{event.button.id}", ColorGroup)
- group.add_class("-active")
- group.scroll_visible(top=True, speed=150)
-
-
-app = ColorsApp()
-
-if __name__ == "__main__":
- app.run()
diff --git a/src/textual/cli/previews/easing.css b/src/textual/cli/previews/easing.css
deleted file mode 100644
index 87607e760..000000000
--- a/src/textual/cli/previews/easing.css
+++ /dev/null
@@ -1,58 +0,0 @@
-Label {
- width: 100%;
-}
-
-EasingButtons > Button {
- width: 100%;
-}
-EasingButtons {
- dock: left;
- overflow-y: scroll;
- width: 20;
-}
-
-#bar-container {
- content-align: center middle;
-}
-
-#duration-input {
- width: 30;
- background: $boost;
- padding: 0 1;
- border: tall transparent;
-}
-
-#duration-input:focus {
- border: tall $accent;
-}
-
-#inputs {
- padding: 1;
- height: auto;
- dock: top;
- background: $boost;
-}
-
-Bar {
- width: 1fr;
-}
-
-#other {
- width: 1fr;
- background: $panel;
- padding: 1;
- height: 100%;
- border-left: vkey $background;
-}
-
-#opacity-widget {
- padding: 1;
- background: $warning;
- color: $text;
- border: wide $background;
-}
-
-#label {
- width: auto;
- padding: 1;
-}
diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py
deleted file mode 100644
index 49baab974..000000000
--- a/src/textual/cli/previews/easing.py
+++ /dev/null
@@ -1,126 +0,0 @@
-from __future__ import annotations
-
-from rich.console import RenderableType
-
-from textual._easing import EASING
-from textual.app import App, ComposeResult
-from textual.cli.previews.borders import TEXT
-from textual.containers import Horizontal, Vertical
-from textual.reactive import reactive, var
-from textual.scrollbar import ScrollBarRender
-from textual.widget import Widget
-from textual.widgets import Button, Footer, Input, Label
-
-VIRTUAL_SIZE = 100
-WINDOW_SIZE = 10
-START_POSITION = 0.0
-END_POSITION = float(VIRTUAL_SIZE - WINDOW_SIZE)
-
-
-class EasingButtons(Widget):
- def compose(self) -> ComposeResult:
- for easing in sorted(EASING, reverse=True):
- yield Button(easing, id=easing)
-
-
-class Bar(Widget):
- position = reactive(START_POSITION)
- animation_running = reactive(False)
-
- DEFAULT_CSS = """
-
- Bar {
- background: $surface;
- color: $error;
- }
-
- Bar.-active {
- background: $surface;
- color: $success;
- }
- """
-
- def watch_animation_running(self, running: bool) -> None:
- self.set_class(running, "-active")
-
- def render(self) -> RenderableType:
- return ScrollBarRender(
- virtual_size=VIRTUAL_SIZE,
- window_size=WINDOW_SIZE,
- position=self.position,
- style=self.rich_style,
- )
-
-
-class EasingApp(App):
- position = reactive(START_POSITION)
- duration = var(1.0)
-
- def on_load(self):
- self.bind(
- "ctrl+p", "focus('duration-input')", description="Focus: Duration Input"
- )
- self.bind("ctrl+b", "toggle_dark", description="Toggle Dark")
-
- def compose(self) -> ComposeResult:
- self.animated_bar = Bar()
- self.animated_bar.position = START_POSITION
- duration_input = Input("1.0", placeholder="Duration", id="duration-input")
-
- self.opacity_widget = Label(
- f"[b]Welcome to Textual![/]\n\n{TEXT}", id="opacity-widget"
- )
-
- yield EasingButtons()
- with Vertical():
- with Horizontal(id="inputs"):
- yield Label("Animation Duration:", id="label")
- yield duration_input
- with Horizontal():
- yield self.animated_bar
- yield Vertical(self.opacity_widget, id="other")
- yield Footer()
-
- def on_button_pressed(self, event: Button.Pressed) -> None:
- self.animated_bar.animation_running = True
-
- def _animation_complete():
- self.animated_bar.animation_running = False
-
- target_position = (
- END_POSITION if self.position == START_POSITION else START_POSITION
- )
- assert event.button.id is not None # Should be set to an easing function str.
- self.animate(
- "position",
- value=target_position,
- final_value=target_position,
- duration=self.duration,
- easing=event.button.id,
- on_complete=_animation_complete,
- )
-
- def watch_position(self, value: int):
- self.animated_bar.position = value
- self.opacity_widget.styles.opacity = 1 - value / END_POSITION
-
- def on_input_changed(self, event: Input.Changed):
- if event.input.id == "duration-input":
- new_duration = _try_float(event.value)
- if new_duration is not None:
- self.duration = new_duration
-
- def action_toggle_dark(self):
- self.dark = not self.dark
-
-
-def _try_float(string: str) -> float | None:
- try:
- return float(string)
- except ValueError:
- return None
-
-
-app = EasingApp(css_path="easing.css")
-if __name__ == "__main__":
- app.run()
diff --git a/src/textual/cli/previews/keys.py b/src/textual/cli/previews/keys.py
deleted file mode 100644
index 4be99e022..000000000
--- a/src/textual/cli/previews/keys.py
+++ /dev/null
@@ -1,72 +0,0 @@
-from __future__ import annotations
-
-from rich.panel import Panel
-from rich.text import Text
-
-from textual import events
-from textual.app import App, ComposeResult
-from textual.containers import Horizontal
-from textual.reactive import Reactive, var
-from textual.widgets import Button, Header, TextLog
-
-INSTRUCTIONS = """\
-[u]Press some keys![/]
-
-To quit the app press [b]ctrl+c[/b] [i]twice[/i] or press the Quit button below.\
-"""
-
-
-class KeyLog(TextLog, inherit_bindings=False):
- """We don't want to handle scroll keys."""
-
-
-class KeysApp(App, inherit_bindings=False):
- """Show key events in a text log."""
-
- TITLE = "Textual Keys"
- BINDINGS = [("c", "clear", "Clear")]
- CSS = """
- #buttons {
- dock: bottom;
- height: 3;
- }
- Button {
- width: 1fr;
- }
- """
-
- last_key: Reactive[str | None] = var(None)
-
- def compose(self) -> ComposeResult:
- yield Header()
- yield Horizontal(
- Button("Clear", id="clear", variant="warning"),
- Button("Quit", id="quit", variant="error"),
- id="buttons",
- )
- yield KeyLog()
-
- def on_ready(self) -> None:
- self.query_one(KeyLog).write(Panel(Text.from_markup(INSTRUCTIONS)), expand=True)
-
- def on_key(self, event: events.Key) -> None:
- self.query_one(KeyLog).write(event)
- if event.key == "ctrl+c":
- if self.last_key == "ctrl+c":
- self.exit()
- else:
- self.query_one(KeyLog).write("Press Ctrl+C again to quit")
-
- self.last_key = event.key
-
- def on_button_pressed(self, event: Button.Pressed) -> None:
- if event.button.id == "quit":
- self.exit()
- elif event.button.id == "clear":
- self.query_one(KeyLog).clear()
-
-
-app = KeysApp()
-
-if __name__ == "__main__":
- app.run()
diff --git a/src/textual/cli/run.py b/src/textual/cli/run.py
deleted file mode 100644
index da3c9ea63..000000000
--- a/src/textual/cli/run.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import sys
-
-from ._run import run
-
-run(sys.argv[1], sys.argv[1:])
diff --git a/src/textual/cli/tools/__init__.py b/src/textual/cli/tools/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/textual/cli/tools/diagnose.py b/src/textual/cli/tools/diagnose.py
deleted file mode 100644
index 54595c955..000000000
--- a/src/textual/cli/tools/diagnose.py
+++ /dev/null
@@ -1,161 +0,0 @@
-"""Textual CLI command code to print diagnostic information."""
-
-from __future__ import annotations
-
-import os
-import platform
-import sys
-from functools import singledispatch
-from typing import Any
-
-from importlib_metadata import version
-from rich.console import Console, ConsoleDimensions
-
-
-def _section(title: str, values: dict[str, str]) -> None:
- """Print a collection of named values within a titled section.
-
- Args:
- title: The title for the section.
- values: The values to print out.
- """
- max_name = max(map(len, values.keys()))
- max_value = max(map(len, values.values()))
- print(f"## {title}")
- print()
- print(f"| {'Name':{max_name}} | {'Value':{max_value}} |")
- print(f"|-{'-' * max_name}-|-{'-'*max_value}-|")
- for name, value in values.items():
- print(f"| {name:{max_name}} | {value:{max_value}} |")
- print()
-
-
-def _versions() -> None:
- """Print useful version numbers."""
- _section("Versions", {"Textual": version("textual"), "Rich": version("rich")})
-
-
-def _python() -> None:
- """Print information about Python."""
- _section(
- "Python",
- {
- "Version": platform.python_version(),
- "Implementation": platform.python_implementation(),
- "Compiler": platform.python_compiler(),
- "Executable": sys.executable,
- },
- )
-
-
-def _os() -> None:
- _section(
- "Operating System",
- {
- "System": platform.system(),
- "Release": platform.release(),
- "Version": platform.version(),
- },
- )
-
-
-def _guess_term() -> str:
- """Try and guess which terminal is being used.
-
- Returns:
- The best guess at the name of the terminal.
- """
-
- # First obvious place to look is in $TERM_PROGRAM.
- term_program = os.environ.get("TERM_PROGRAM")
-
- if term_program is None:
- # Seems we couldn't get it that way. Let's check for some of the
- # more common terminal signatures.
- if "ALACRITTY_WINDOW_ID" in os.environ:
- term_program = "Alacritty"
- elif "KITTY_PID" in os.environ:
- term_program = "Kitty"
- elif "WT_SESSION" in os.environ:
- term_program = "Windows Terminal"
- elif "INSIDE_EMACS" in os.environ and os.environ["INSIDE_EMACS"]:
- term_program = (
- f"GNU Emacs {' '.join(os.environ['INSIDE_EMACS'].split(','))}"
- )
- elif "JEDITERM_SOURCE_ARGS" in os.environ:
- term_program = "PyCharm"
-
- else:
- # See if we can pull out some sort of version information too.
- term_version = os.environ.get("TERM_PROGRAM_VERSION")
- if term_version is not None:
- term_program = f"{term_program} ({term_version})"
-
- return "*Unknown*" if term_program is None else term_program
-
-
-def _env(var_name: str) -> str:
- """Get a representation of an environment variable.
-
- Args:
- var_name: The name of the variable to get.
-
- Returns:
- The value, or an indication that it isn't set.
- """
- return os.environ.get(var_name, "*Not set*")
-
-
-def _term() -> None:
- """Print information about the terminal."""
- _section(
- "Terminal",
- {
- "Terminal Application": _guess_term(),
- "TERM": _env("TERM"),
- "COLORTERM": _env("COLORTERM"),
- "FORCE_COLOR": _env("FORCE_COLOR"),
- "NO_COLOR": _env("NO_COLOR"),
- },
- )
-
-
-@singledispatch
-def _str_rich(value: Any) -> str:
- """Convert a rich console option to a string.
-
- Args:
- value: The value to convert to a string.
-
- Returns:
- The string version of the value for output
- """
- return str(value)
-
-
-@_str_rich.register
-def _(value: ConsoleDimensions) -> str:
- return f"width={value.width}, height={value.height}"
-
-
-def _console() -> None:
- """Print The Rich console options."""
- _section(
- "Rich Console options",
- {k: _str_rich(v) for k, v in Console().options.__dict__.items()},
- )
-
-
-def diagnose() -> None:
- """Print information about Textual and its environment to help diagnose problems."""
- print("")
- print("# Textual Diagnostics")
- print()
- _versions()
- _python()
- _os()
- _term()
- _console()
- # TODO: Recommended changes. Given all of the above, make any useful
- # recommendations to the user (eg: don't use Windows console, use
- # Windows Terminal; don't use macOS Terminal.app, etc).
diff --git a/src/textual/devtools/__init__.py b/src/textual/devtools/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/textual/devtools/client.py b/src/textual/devtools/client.py
deleted file mode 100644
index 1887d41ed..000000000
--- a/src/textual/devtools/client.py
+++ /dev/null
@@ -1,265 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-import inspect
-import json
-import pickle
-from asyncio import Queue, QueueFull, Task
-from io import StringIO
-from time import time
-from typing import Any, NamedTuple, Type
-
-import aiohttp
-import msgpack
-from aiohttp import ClientConnectorError, ClientResponseError, ClientWebSocketResponse
-from rich.console import Console
-from rich.segment import Segment
-
-from .._log import LogGroup, LogVerbosity
-from ..constants import DEVTOOLS_PORT
-
-WEBSOCKET_CONNECT_TIMEOUT = 3
-LOG_QUEUE_MAXSIZE = 512
-
-
-class DevtoolsLog(NamedTuple):
- """A devtools log message.
-
- Attributes:
- objects_or_string: Corresponds to the data that will
- ultimately be passed to Console.print in order to generate the log
- Segments.
- caller: Information about where this log message was
- created. In other words, where did the user call `print` or `App.log`
- from. Used to display line number and file name in the devtools window.
- """
-
- objects_or_string: tuple[Any, ...] | str
- caller: inspect.Traceback
-
-
-class DevtoolsConsole(Console):
- def __init__(self, *args, **kwargs) -> None:
- super().__init__(*args, **kwargs)
- self.record = True
-
- def export_segments(self) -> list[Segment]:
- """Return the list of Segments that have be printed using this console
-
- Returns:
- The list of Segments that have been printed using this console
- """
- with self._record_buffer_lock:
- segments = self._record_buffer[:]
- self._record_buffer.clear()
- return segments
-
-
-class DevtoolsConnectionError(Exception):
- """Raise when the devtools client is unable to connect to the server"""
-
-
-class ClientShutdown:
- """Sentinel type sent to client queue(s) to indicate shutdown"""
-
-
-class DevtoolsClient:
- """Client responsible for websocket communication with the devtools server.
- Communicates using a simple JSON protocol.
-
- Messages have the format `{"type": , "payload": }`.
-
- Valid values for `"type"` (that can be sent from client -> server) are
- `"client_log"` (for log messages) and `"client_spillover"` (for reporting
- to the server that messages were discarded due to rate limiting).
-
- A `"client_log"` message has a `"payload"` format as follows:
- ```
- {"timestamp": ,
- "path": ,
- "line_number": ,
- "encoded_segments": }
- ```
-
- A `"client_spillover"` message has a `"payload"` format as follows:
- ```
- {"spillover": }
- ```
-
- Args:
- host: The host the devtools server is running on, defaults to "127.0.0.1"
- port: The port the devtools server is accessed via, `DEVTOOLS_PORT` by default.
- """
-
- def __init__(self, host: str = "127.0.0.1", port: int | None = None) -> None:
- if port is None:
- port = DEVTOOLS_PORT
- self.url: str = f"ws://{host}:{port}"
- self.session: aiohttp.ClientSession | None = None
- self.log_queue_task: Task | None = None
- self.update_console_task: Task | None = None
- self.console: DevtoolsConsole = DevtoolsConsole(file=StringIO())
- self.websocket: ClientWebSocketResponse | None = None
- self.log_queue: Queue[str | bytes | Type[ClientShutdown]] | None = None
- self.spillover: int = 0
- self.verbose: bool = False
-
- async def connect(self) -> None:
- """Connect to the devtools server.
-
- Raises:
- DevtoolsConnectionError: If we're unable to establish
- a connection to the server for any reason.
- """
- self.session = aiohttp.ClientSession()
- self.log_queue = Queue(maxsize=LOG_QUEUE_MAXSIZE)
- try:
- self.websocket = await self.session.ws_connect(
- f"{self.url}/textual-devtools-websocket",
- timeout=WEBSOCKET_CONNECT_TIMEOUT,
- )
- except (ClientConnectorError, ClientResponseError):
- raise DevtoolsConnectionError()
-
- log_queue = self.log_queue
- websocket = self.websocket
-
- async def update_console() -> None:
- """Coroutine function scheduled as a Task, which listens on
- the websocket for updates from the server regarding any changes
- in the server Console dimensions. When the client learns of this
- change, it will update its own Console to ensure it renders at
- the correct width for server-side display.
- """
- assert self.websocket is not None
- async for message in self.websocket:
- if message.type == aiohttp.WSMsgType.TEXT:
- message_json = json.loads(message.data)
- if message_json["type"] == "server_info":
- payload = message_json["payload"]
- self.console.width = payload["width"]
- self.console.height = payload["height"]
- self.verbose = payload.get("verbose", False)
-
- async def send_queued_logs():
- """Coroutine function which is scheduled as a Task, which consumes
- messages from the log queue and sends them to the server via websocket.
- """
- while True:
- log = await log_queue.get()
- if log is ClientShutdown:
- log_queue.task_done()
- break
- if isinstance(log, str):
- await websocket.send_str(log)
- else:
- assert isinstance(log, bytes)
- await websocket.send_bytes(log)
- log_queue.task_done()
-
- self.log_queue_task = asyncio.create_task(send_queued_logs())
- self.update_console_task = asyncio.create_task(update_console())
-
- async def _stop_log_queue_processing(self) -> None:
- """Schedule end of processing of the log queue, meaning that any messages a
- user logs will be added to the queue, but not consumed and sent to
- the server.
- """
- if self.log_queue is not None:
- await self.log_queue.put(ClientShutdown)
- if self.log_queue_task:
- await self.log_queue_task
-
- async def _stop_incoming_message_processing(self) -> None:
- """Schedule stop of the task which listens for incoming messages from the
- server around changes in the server console size.
- """
- if self.websocket:
- await self.websocket.close()
- if self.update_console_task:
- await self.update_console_task
- if self.session:
- await self.session.close()
-
- async def disconnect(self) -> None:
- """Disconnect from the devtools server by stopping tasks and
- closing connections.
- """
- await self._stop_log_queue_processing()
- await self._stop_incoming_message_processing()
-
- @property
- def is_connected(self) -> bool:
- """Checks connection to devtools server.
-
- Returns:
- True if this host is connected to the server. False otherwise.
- """
- if not self.session or not self.websocket:
- return False
- return not (self.session.closed or self.websocket.closed)
-
- def log(
- self,
- log: DevtoolsLog,
- group: LogGroup = LogGroup.UNDEFINED,
- verbosity: LogVerbosity = LogVerbosity.NORMAL,
- ) -> None:
- """Queue a log to be sent to the devtools server for display.
-
- Args:
- log: The log to write to devtools
- """
- if isinstance(log.objects_or_string, str):
- self.console.print(log.objects_or_string, markup=False)
- else:
- self.console.print(*log.objects_or_string, markup=False)
-
- segments = self.console.export_segments()
-
- encoded_segments = self._encode_segments(segments)
- message: bytes | None = msgpack.packb(
- {
- "type": "client_log",
- "payload": {
- "group": group.value,
- "verbosity": verbosity.value,
- "timestamp": int(time()),
- "path": getattr(log.caller, "filename", ""),
- "line_number": getattr(log.caller, "lineno", 0),
- "segments": encoded_segments,
- },
- }
- )
- assert message is not None
- try:
- if self.log_queue:
- self.log_queue.put_nowait(message)
- if self.spillover > 0 and self.log_queue.qsize() < LOG_QUEUE_MAXSIZE:
- # Tell the server how many messages we had to discard due
- # to the log queue filling to capacity on the client.
- spillover_message = json.dumps(
- {
- "type": "client_spillover",
- "payload": {
- "spillover": self.spillover,
- },
- }
- )
- self.log_queue.put_nowait(spillover_message)
- self.spillover = 0
- except QueueFull:
- self.spillover += 1
-
- @classmethod
- def _encode_segments(cls, segments: list[Segment]) -> bytes:
- """Pickle a list of Segments
-
- Args:
- segments: A list of Segments to encode
-
- Returns:
- The Segment list pickled with the latest protocol.
- """
- pickled = pickle.dumps(segments, protocol=4)
- return pickled
diff --git a/src/textual/devtools/redirect_output.py b/src/textual/devtools/redirect_output.py
deleted file mode 100644
index 0e1f934b2..000000000
--- a/src/textual/devtools/redirect_output.py
+++ /dev/null
@@ -1,109 +0,0 @@
-from __future__ import annotations
-
-import inspect
-from typing import TYPE_CHECKING, cast
-
-from .._log import LogGroup, LogVerbosity
-from .client import DevtoolsLog
-
-if TYPE_CHECKING:
- from .client import DevtoolsClient
-
-
-class StdoutRedirector:
- """
- A write-only file-like object which redirects anything written to it to the devtools
- instance associated with the given Textual application. Used within Textual to redirect
- data written using `print` (or any other stdout writes) to the devtools and/or to the
- log file.
- """
-
- def __init__(self, devtools: DevtoolsClient) -> None:
- """
- Args:
- devtools: The running Textual app instance.
- log_file: The log file for the Textual App.
- """
- self.devtools = devtools
- self._buffer: list[DevtoolsLog] = []
-
- def write(self, string: str) -> None:
- """Write the log string to the internal buffer. If the string contains
- a newline character `\n`, the whole string will be buffered and then the
- buffer will be flushed immediately after.
-
- Args:
- string: The string to write to the buffer.
- """
-
- if not self.devtools.is_connected:
- return
-
- current_frame = inspect.currentframe()
- assert current_frame is not None
- previous_frame = current_frame.f_back
- assert previous_frame is not None
- caller = inspect.getframeinfo(previous_frame)
-
- self._buffer.append(DevtoolsLog(string, caller=caller))
-
- # By default, `print` adds a "\n" suffix which results in a buffer
- # flush. You can choose a different suffix with the `end` parameter.
- # If you modify the `end` parameter to something other than "\n",
- # then `print` will no longer flush automatically. However, if a
- # string you are printing contains a "\n", that will trigger
- # a flush after that string has been buffered, regardless of the value
- # of `end`.
- if "\n" in string:
- self.flush()
-
- def flush(self) -> None:
- """Flush the buffer. This will send all buffered log messages to
- the devtools server and the log file. In the case of the devtools,
- where possible, log messages will be batched and sent as one.
- """
- self._write_to_devtools()
- self._buffer.clear()
-
- def _write_to_devtools(self) -> None:
- """Send the contents of the buffer to the devtools."""
- if not self.devtools.is_connected:
- return
-
- log_batch: list[DevtoolsLog] = []
- for log in self._buffer:
- end_of_batch = log_batch and (
- log_batch[-1].caller.filename != log.caller.filename
- or log_batch[-1].caller.lineno != log.caller.lineno
- )
- if end_of_batch:
- self._log_devtools_batched(log_batch)
- log_batch.clear()
- log_batch.append(log)
- if log_batch:
- self._log_devtools_batched(log_batch)
-
- def _log_devtools_batched(self, log_batch: list[DevtoolsLog]) -> None:
- """Write a single batch of logs to devtools. A batch means contiguous logs
- which have been written from the same line number and file path.
- A single `print` call may correspond to multiple writes.
- e.g. `print("a", "b", "c")` is 3 calls to `write`, so we batch
- up these 3 write calls since they come from the same location, so that
- they appear inside the same log message in the devtools window
- rather than a single `print` statement resulting in 3 separate
- logs being displayed.
-
- Args:
- log_batch: A batch of logs to send to the
- devtools server as one. Log content will be joined together.
- """
-
- # This code is only called via stdout.write, and so by this point we know
- # that the log message content is a string. The cast below tells mypy this.
- batched_log = "".join(cast(str, log.objects_or_string) for log in log_batch)
- batched_log = batched_log.rstrip()
- self.devtools.log(
- DevtoolsLog(batched_log, caller=log_batch[-1].caller),
- LogGroup.PRINT,
- LogVerbosity.NORMAL,
- )
diff --git a/src/textual/devtools/renderables.py b/src/textual/devtools/renderables.py
deleted file mode 100644
index a8ddb166e..000000000
--- a/src/textual/devtools/renderables.py
+++ /dev/null
@@ -1,134 +0,0 @@
-from __future__ import annotations
-
-from datetime import datetime
-from pathlib import Path
-from typing import Iterable
-
-from importlib_metadata import version
-from rich.align import Align
-from rich.console import Console, ConsoleOptions, RenderResult
-from rich.markup import escape
-from rich.rule import Rule
-from rich.segment import Segment, Segments
-from rich.style import Style
-from rich.styled import Styled
-from rich.table import Table
-from rich.text import Text
-from typing_extensions import Literal
-
-from textual._log import LogGroup
-
-DevConsoleMessageLevel = Literal["info", "warning", "error"]
-
-
-class DevConsoleHeader:
- def __init__(self, verbose: bool = False) -> None:
- self.verbose = verbose
-
- def __rich_console__(
- self, console: Console, options: ConsoleOptions
- ) -> RenderResult:
- preamble = Text.from_markup(
- f"[bold]Textual Development Console [magenta]v{version('textual')}\n"
- "[magenta]Run a Textual app with [reverse]textual run --dev my_app.py[/] to connect.\n"
- "[magenta]Press [reverse]Ctrl+C[/] to quit."
- )
- if self.verbose:
- preamble.append(Text.from_markup("\n[cyan]Verbose logs enabled"))
- render_options = options.update(width=options.max_width - 4)
- lines = console.render_lines(preamble, render_options)
-
- new_line = Segment.line()
- padding = Segment("▌", Style.parse("bright_magenta"))
-
- for line in lines:
- yield padding
- yield from line
- yield new_line
-
-
-class DevConsoleLog:
- """Renderable representing a single log message
-
- Args:
- segments: The segments to display
- path: The path of the file on the client that the log call was made from
- line_number: The line number of the file on the client the log call was made from
- unix_timestamp: Seconds since January 1st 1970
- """
-
- def __init__(
- self,
- segments: Iterable[Segment],
- path: str,
- line_number: int,
- unix_timestamp: int,
- group: int,
- verbosity: int,
- severity: int,
- ) -> None:
- self.segments = segments
- self.path = path
- self.line_number = line_number
- self.unix_timestamp = unix_timestamp
- self.group = group
- self.verbosity = verbosity
- self.severity = severity
-
- def __rich_console__(
- self, console: Console, options: ConsoleOptions
- ) -> RenderResult:
- local_time = datetime.fromtimestamp(self.unix_timestamp)
- table = Table.grid(expand=True)
-
- file_link = escape(f"file://{Path(self.path).absolute()}")
- file_and_line = escape(f"{Path(self.path).name}:{self.line_number}")
- group = LogGroup(self.group).name
- time = local_time.time()
-
- group_text = Text(group)
- if group == "WARNING":
- group_text.stylize("bold yellow reverse")
- elif group == "ERROR":
- group_text.stylize("bold red reverse")
- else:
- group_text.stylize("dim")
-
- log_message = Text.assemble((f"[{time}]", "dim"), " ", group_text)
-
- table.add_row(
- log_message,
- Align.right(
- Text(f"{file_and_line}", style=Style(dim=True, link=file_link))
- ),
- )
- yield table
-
- if group == "PRINT":
- yield Styled(Segments(self.segments), "bold")
- else:
- yield from self.segments
-
-
-class DevConsoleNotice:
- """Renderable for messages written by the devtools console itself
-
- Args:
- message: The message to display
- level: The message level ("info", "warning", or "error").
- Determines colors used to render the message and the perceived importance.
- """
-
- def __init__(self, message: str, *, level: DevConsoleMessageLevel = "info") -> None:
- self.message = message
- self.level = level
-
- def __rich_console__(
- self, console: Console, options: ConsoleOptions
- ) -> RenderResult:
- level_to_style = {
- "info": "dim",
- "warning": "yellow",
- "error": "red",
- }
- yield Rule(self.message, style=level_to_style.get(self.level, "dim"))
diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py
deleted file mode 100644
index 11f631f00..000000000
--- a/src/textual/devtools/server.py
+++ /dev/null
@@ -1,86 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-
-from aiohttp.web import run_app
-from aiohttp.web_app import Application
-from aiohttp.web_request import Request
-from aiohttp.web_routedef import get
-from aiohttp.web_ws import WebSocketResponse
-
-from ..constants import DEVTOOLS_PORT
-from .client import DEVTOOLS_PORT
-from .service import DevtoolsService
-
-DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS = 2
-
-
-async def websocket_handler(request: Request) -> WebSocketResponse:
- """aiohttp websocket handler for sending data between devtools client and server
-
- Args:
- request: The request to the websocket endpoint
-
- Returns:
- The websocket response
- """
- service: DevtoolsService = request.app["service"]
- return await service.handle(request)
-
-
-async def _on_shutdown(app: Application) -> None:
- """aiohttp shutdown handler, called when the aiohttp server is stopped"""
- service: DevtoolsService = app["service"]
- await service.shutdown()
-
-
-async def _on_startup(app: Application) -> None:
- service: DevtoolsService = app["service"]
- await service.start()
-
-
-def _run_devtools(
- verbose: bool, exclude: list[str] | None = None, port: int | None = None
-) -> None:
- app = _make_devtools_aiohttp_app(verbose=verbose, exclude=exclude)
-
- def noop_print(_: str) -> None:
- pass
-
- try:
- run_app(
- app,
- port=DEVTOOLS_PORT if port is None else port,
- print=noop_print,
- loop=asyncio.get_event_loop(),
- )
- except OSError:
- from rich import print
-
- print()
- print("[bold red]Couldn't start server")
- print("Is there another instance of [reverse]textual console[/] running?")
-
-
-def _make_devtools_aiohttp_app(
- size_change_poll_delay_secs: float = DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS,
- verbose: bool = False,
- exclude: list[str] | None = None,
-) -> Application:
- app = Application()
-
- app.on_shutdown.append(_on_shutdown)
- app.on_startup.append(_on_startup)
-
- app["verbose"] = verbose
- app["service"] = DevtoolsService(
- update_frequency=size_change_poll_delay_secs, verbose=verbose, exclude=exclude
- )
-
- app.add_routes(
- [
- get("/textual-devtools-websocket", websocket_handler),
- ]
- )
-
- return app
diff --git a/src/textual/devtools/service.py b/src/textual/devtools/service.py
deleted file mode 100644
index 1559c8efa..000000000
--- a/src/textual/devtools/service.py
+++ /dev/null
@@ -1,289 +0,0 @@
-"""Manages a running devtools instance"""
-from __future__ import annotations
-
-import asyncio
-import json
-import pickle
-from json import JSONDecodeError
-from typing import Any
-
-import msgpack
-from aiohttp import WSMsgType
-from aiohttp.abc import Request
-from aiohttp.web_ws import WebSocketResponse
-from rich.console import Console
-from rich.markup import escape
-
-from textual._log import LogGroup
-from textual._time import time
-from textual.devtools.renderables import (
- DevConsoleHeader,
- DevConsoleLog,
- DevConsoleNotice,
-)
-
-QUEUEABLE_TYPES = {"client_log", "client_spillover"}
-
-
-class DevtoolsService:
- """A running instance of devtools has a single DevtoolsService which is
- responsible for tracking connected client applications.
- """
-
- def __init__(
- self,
- update_frequency: float,
- verbose: bool = False,
- exclude: list[str] | None = None,
- ) -> None:
- """
- Args:
- update_frequency: The number of seconds to wait between
- sending updates of the console size to connected clients.
- verbose: Enable verbose logging on client.
- exclude: List of log groups to exclude from output.
- """
- self.update_frequency = update_frequency
- self.verbose = verbose
- self.exclude = {name.upper() for name in exclude} if exclude else set()
- self.console = Console()
- self.shutdown_event = asyncio.Event()
- self.clients: list[ClientHandler] = []
-
- async def start(self):
- """Starts devtools tasks"""
- self.size_poll_task = asyncio.create_task(self._console_size_poller())
- self.console.print(DevConsoleHeader(verbose=self.verbose))
-
- @property
- def clients_connected(self) -> bool:
- """Returns True if there are connected clients, False otherwise."""
- return len(self.clients) > 0
-
- async def _console_size_poller(self) -> None:
- """Poll console dimensions, and add a `server_info` message to the Queue
- any time a change occurs. We only poll if there are clients connected,
- and if we're not shutting down the server.
- """
- current_width = self.console.width
- current_height = self.console.height
- await self._send_server_info_to_all()
- while not self.shutdown_event.is_set():
- width = self.console.width
- height = self.console.height
- dimensions_changed = width != current_width or height != current_height
- if dimensions_changed:
- await self._send_server_info_to_all()
- current_width = width
- current_height = height
- try:
- await asyncio.wait_for(
- self.shutdown_event.wait(), timeout=self.update_frequency
- )
- except asyncio.TimeoutError:
- pass
-
- async def _send_server_info_to_all(self) -> None:
- """Add `server_info` message to the queues of every client"""
- for client_handler in self.clients:
- await self.send_server_info(client_handler)
-
- async def send_server_info(self, client_handler: ClientHandler) -> None:
- """Send information about the server e.g. width and height of Console to
- a connected client.
-
- Args:
- client_handler: The client to send information to
- """
- await client_handler.send_message(
- {
- "type": "server_info",
- "payload": {
- "width": self.console.width,
- "height": self.console.height,
- "verbose": self.verbose,
- },
- }
- )
-
- async def handle(self, request: Request) -> WebSocketResponse:
- """Handles a single client connection"""
- client = ClientHandler(request, service=self)
- self.clients.append(client)
- websocket = await client.run()
- self.clients.remove(client)
- return websocket
-
- async def shutdown(self) -> None:
- """Stop server async tasks and clean up all client handlers"""
-
- # Stop polling/writing Console dimensions to clients
- self.shutdown_event.set()
- await self.size_poll_task
-
- # We're shutting down the server, so inform all connected clients
- for client in self.clients:
- await client.close()
- self.clients.clear()
-
-
-class ClientHandler:
- """Handles a single client connection to the devtools.
- A single DevtoolsService managers many ClientHandlers. A single ClientHandler
- corresponds to a single running Textual application instance, and is responsible
- for communication with that Textual app.
- """
-
- def __init__(self, request: Request, service: DevtoolsService) -> None:
- """
- Args:
- request: The aiohttp.Request associated with this client
- service: The parent DevtoolsService which is responsible
- for the handling of this client.
- """
- self.request = request
- self.service = service
- self.websocket = WebSocketResponse()
-
- async def send_message(self, message: dict[str, object]) -> None:
- """Send a message to a client
-
- Args:
- message: The dict which will be sent
- to the client.
- """
- await self.outgoing_queue.put(message)
-
- async def _consume_outgoing(self) -> None:
- """Consume messages from the outgoing (server -> client) Queue."""
- while True:
- message_json = await self.outgoing_queue.get()
- if message_json is None:
- self.outgoing_queue.task_done()
- break
- type = message_json["type"]
- if type == "server_info":
- await self.websocket.send_json(message_json)
- self.outgoing_queue.task_done()
-
- async def _consume_incoming(self) -> None:
- """Consume messages from the incoming (client -> server) Queue, and print
- the corresponding renderables to the console for each message.
- """
- last_message_time: float | None = None
- while True:
- message = await self.incoming_queue.get()
- if message is None:
- self.incoming_queue.task_done()
- break
-
- type = message["type"]
- if type == "client_log":
- payload = message["payload"]
- if LogGroup(payload.get("group", 0)).name in self.service.exclude:
- continue
- encoded_segments = payload["segments"]
- segments = pickle.loads(encoded_segments)
- message_time = time()
- if (
- last_message_time is not None
- and message_time - last_message_time > 0.5
- ):
- # Print a rule if it has been longer than half a second since the last message
- self.service.console.rule()
- self.service.console.print(
- DevConsoleLog(
- segments=segments,
- path=payload["path"],
- line_number=payload["line_number"],
- unix_timestamp=payload["timestamp"],
- group=payload.get("group", 0),
- verbosity=payload.get("verbosity", 0),
- severity=payload.get("severity", 0),
- )
- )
- last_message_time = message_time
- elif type == "client_spillover":
- spillover = int(message["payload"]["spillover"])
- info_renderable = DevConsoleNotice(
- f"Discarded {spillover} messages", level="warning"
- )
- self.service.console.print(info_renderable)
- self.incoming_queue.task_done()
-
- async def run(self) -> WebSocketResponse:
- """Prepare the websocket and communication queues, and continuously
- read messages from the queues.
-
- Returns:
- The WebSocketResponse associated with this client.
- """
-
- await self.websocket.prepare(self.request)
- self.incoming_queue: asyncio.Queue[dict | None] = asyncio.Queue()
- self.outgoing_queue: asyncio.Queue[dict | None] = asyncio.Queue()
- self.outgoing_messages_task = asyncio.create_task(self._consume_outgoing())
- self.incoming_messages_task = asyncio.create_task(self._consume_incoming())
-
- if self.request.remote:
- self.service.console.print(
- DevConsoleNotice(f"Client '{escape(self.request.remote)}' connected")
- )
- try:
- await self.service.send_server_info(client_handler=self)
- async for websocket_message in self.websocket:
- if websocket_message.type in (WSMsgType.TEXT, WSMsgType.BINARY):
- message: dict[str, Any]
- try:
- if isinstance(websocket_message.data, bytes):
- message = msgpack.unpackb(websocket_message.data)
- else:
- message = json.loads(websocket_message.data)
- except JSONDecodeError:
- self.service.console.print(escape(str(websocket_message.data)))
- continue
-
- type = message.get("type")
- if not type:
- continue
- if (
- type in QUEUEABLE_TYPES
- and not self.service.shutdown_event.is_set()
- ):
- await self.incoming_queue.put(message)
- elif websocket_message.type == WSMsgType.ERROR:
- self.service.console.print(websocket_message.data)
- self.service.console.print(
- DevConsoleNotice("Websocket error occurred", level="error")
- )
- break
- except Exception as error:
- self.service.console.print(DevConsoleNotice(str(error), level="error"))
- finally:
- if self.request.remote:
- self.service.console.print(
- "\n",
- DevConsoleNotice(
- f"Client '{escape(self.request.remote)}' disconnected"
- ),
- )
- await self.close()
-
- return self.websocket
-
- async def close(self) -> None:
- """Stop all incoming/outgoing message processing,
- and shutdown the websocket connection associated with this
- client.
- """
-
- # Stop any writes to the websocket first
- await self.outgoing_queue.put(None)
- await self.outgoing_messages_task
-
- # Now we can shut the socket down
- await self.websocket.close()
-
- # This task is independent of the websocket
- await self.incoming_queue.put(None)
- await self.incoming_messages_task
diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py
deleted file mode 100644
index 3909ba67c..000000000
--- a/tests/cli/test_cli.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from click.testing import CliRunner
-from importlib_metadata import version
-
-from textual.cli.cli import run
-
-
-def test_cli_version():
- runner = CliRunner()
- result = runner.invoke(run, ["--version"])
- assert version("textual") in result.output
diff --git a/tests/devtools/__init__.py b/tests/devtools/__init__.py
deleted file mode 100644
index 9e35d1a25..000000000
--- a/tests/devtools/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import os
-import sys
-
-import pytest
-
-_MACOS_CI = sys.platform == "darwin" and os.getenv("CI", "0") != "0"
-_WINDOWS = sys.platform == "win32"
-
-# TODO - this needs to be revisited - perhaps when aiohttp 4.0 is released?
-# We get occasional test failures relating to devtools. These *appear* to be limited to MacOS,
-# and the error messages suggest the event loop is being shutdown before async fixture
-# teardown code has finished running. These are very rare, but are much more of an issue on
-# CI since they can delay builds that have passed locally.
-pytestmark = pytest.mark.skipif(_MACOS_CI or _WINDOWS, reason="Issue #411")
diff --git a/tests/devtools/conftest.py b/tests/devtools/conftest.py
deleted file mode 100644
index 5b24a0655..000000000
--- a/tests/devtools/conftest.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import pytest
-
-from textual.devtools.client import DevtoolsClient
-from textual.devtools.server import _make_devtools_aiohttp_app
-from textual.devtools.service import DevtoolsService
-
-
-@pytest.fixture
-async def server(aiohttp_server, unused_tcp_port):
- app = _make_devtools_aiohttp_app(
- size_change_poll_delay_secs=0.001,
- )
- server = await aiohttp_server(app, port=unused_tcp_port)
- service: DevtoolsService = app["service"]
- yield server
- await service.shutdown()
- await server.close()
-
-
-@pytest.fixture
-async def devtools(aiohttp_client, server):
- client = await aiohttp_client(server)
- devtools = DevtoolsClient(host=client.host, port=client.port)
- await devtools.connect()
- yield devtools
- await devtools.disconnect()
- await client.close()
diff --git a/tests/devtools/test_devtools.py b/tests/devtools/test_devtools.py
deleted file mode 100644
index 944de3fca..000000000
--- a/tests/devtools/test_devtools.py
+++ /dev/null
@@ -1,96 +0,0 @@
-from datetime import datetime
-
-import msgpack
-import pytest
-import time_machine
-from rich.align import Align
-from rich.console import Console
-from rich.segment import Segment
-
-from tests.utilities.render import wait_for_predicate
-from textual.devtools.renderables import DevConsoleLog, DevConsoleNotice
-
-TIMESTAMP = 1649166819
-WIDTH = 40
-# The string "Hello, world!" is encoded in the payload below
-_EXAMPLE_LOG = {
- "type": "client_log",
- "payload": {
- "segments": b"\x80\x04\x955\x00\x00\x00\x00\x00\x00\x00]\x94\x8c\x0crich.segment\x94\x8c\x07Segment\x94\x93\x94\x8c\rHello, world!\x94NN\x87\x94\x81\x94a.",
- "line_number": 123,
- "path": "abc/hello.py",
- "timestamp": TIMESTAMP,
- },
-}
-EXAMPLE_LOG = msgpack.packb(_EXAMPLE_LOG)
-
-
-@pytest.fixture(scope="module")
-def console():
- return Console(width=WIDTH)
-
-
-@time_machine.travel(TIMESTAMP)
-def test_log_message_render(console):
- message = DevConsoleLog(
- [Segment("content")],
- path="abc/hello.py",
- line_number=123,
- unix_timestamp=TIMESTAMP,
- group=0,
- verbosity=0,
- severity=0,
- )
- table = next(iter(message.__rich_console__(console, console.options)))
-
- assert len(table.rows) == 1
-
- columns = list(table.columns)
- left_cells = list(columns[0].cells)
- left = left_cells[0]
- right_cells = list(columns[1].cells)
- right: Align = right_cells[0]
-
- # Since we can't guarantee the timezone the tests will run in...
- local_time = datetime.fromtimestamp(TIMESTAMP)
- string_timestamp = local_time.time()
-
- assert left.plain == f"[{string_timestamp}] UNDEFINED"
- assert right.align == "right"
- assert "hello.py:123" in right.renderable
-
-
-def test_internal_message_render(console):
- message = DevConsoleNotice("hello")
- rule = next(iter(message.__rich_console__(console, console.options)))
- assert rule.title == "hello"
- assert rule.characters == "─"
-
-
-async def test_devtools_valid_client_log(devtools):
- await devtools.websocket.send_bytes(EXAMPLE_LOG)
- assert devtools.is_connected
-
-
-async def test_devtools_string_not_json_message(devtools):
- await devtools.websocket.send_str("ABCDEFG")
- assert devtools.is_connected
-
-
-async def test_devtools_invalid_json_message(devtools):
- await devtools.websocket.send_json({"invalid": "json"})
- assert devtools.is_connected
-
-
-async def test_devtools_spillover_message(devtools):
- await devtools.websocket.send_json(
- {"type": "client_spillover", "payload": {"spillover": 123}}
- )
- assert devtools.is_connected
-
-
-async def test_devtools_console_size_change(server, devtools):
- # Update the width of the console on the server-side
- server.app["service"].console.width = 124
- # Wait for the client side to update the console on their end
- await wait_for_predicate(lambda: devtools.console.width == 124)
diff --git a/tests/devtools/test_devtools_client.py b/tests/devtools/test_devtools_client.py
deleted file mode 100644
index cc5231de7..000000000
--- a/tests/devtools/test_devtools_client.py
+++ /dev/null
@@ -1,107 +0,0 @@
-import json
-import types
-from asyncio import Queue
-from datetime import datetime
-
-import msgpack
-import time_machine
-from aiohttp.web_ws import WebSocketResponse
-from rich.console import ConsoleDimensions
-from rich.panel import Panel
-
-from tests.utilities.render import wait_for_predicate
-from textual.constants import DEVTOOLS_PORT
-from textual.devtools.client import DevtoolsClient
-from textual.devtools.redirect_output import DevtoolsLog
-
-CALLER_LINENO = 123
-CALLER_PATH = "a/b/c.py"
-CALLER = types.SimpleNamespace(filename=CALLER_PATH, lineno=CALLER_LINENO)
-TIMESTAMP = 1649166819
-
-
-def test_devtools_client_initialize_defaults():
- devtools = DevtoolsClient()
- assert devtools.url == f"ws://127.0.0.1:{DEVTOOLS_PORT}"
-
-
-async def test_devtools_client_is_connected(devtools):
- assert devtools.is_connected
-
-
-@time_machine.travel(datetime.utcfromtimestamp(TIMESTAMP))
-async def test_devtools_log_places_encodes_and_queues_message(devtools):
- await devtools._stop_log_queue_processing()
- devtools.log(DevtoolsLog("Hello, world!", CALLER))
- queued_log = await devtools.log_queue.get()
- queued_log_data = msgpack.unpackb(queued_log)
- print(repr(queued_log_data))
-
-
-@time_machine.travel(datetime.utcfromtimestamp(TIMESTAMP))
-async def test_devtools_log_places_encodes_and_queues_many_logs_as_string(devtools):
- await devtools._stop_log_queue_processing()
- devtools.log(DevtoolsLog(("hello", "world"), CALLER))
- queued_log = await devtools.log_queue.get()
- queued_log_data = msgpack.unpackb(queued_log)
- print(repr(queued_log_data))
- assert queued_log_data == {
- "type": "client_log",
- "payload": {
- "group": 0,
- "verbosity": 0,
- "timestamp": 1649166819,
- "path": "a/b/c.py",
- "line_number": 123,
- "segments": b"\x80\x04\x95@\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x0crich.segment\x94\x8c\x07Segment\x94\x93\x94\x8c\x0bhello world\x94NN\x87\x94\x81\x94h\x03\x8c\x01\n\x94NN\x87\x94\x81\x94e.",
- },
- }
-
-
-async def test_devtools_log_spillover(devtools):
- # Give the devtools an intentionally small max queue size
- await devtools._stop_log_queue_processing()
- devtools.log_queue = Queue(maxsize=2)
-
- # Force spillover of 2
- devtools.log(DevtoolsLog((Panel("hello, world"),), CALLER))
- devtools.log(DevtoolsLog("second message", CALLER))
- devtools.log(DevtoolsLog("third message", CALLER)) # Discarded by rate-limiting
- devtools.log(DevtoolsLog("fourth message", CALLER)) # Discarded by rate-limiting
-
- assert devtools.spillover == 2
-
- # Consume log queue
- while not devtools.log_queue.empty():
- await devtools.log_queue.get()
-
- # Add another message now that we're under spillover threshold
- devtools.log(DevtoolsLog("another message", CALLER))
- await devtools.log_queue.get()
-
- # Ensure we're informing the server of spillover rate-limiting
- spillover_message = await devtools.log_queue.get()
- assert json.loads(spillover_message) == {
- "type": "client_spillover",
- "payload": {"spillover": 2},
- }
-
-
-async def test_devtools_client_update_console_dimensions(devtools, server):
- """Sending new server info through websocket from server to client should (eventually)
- result in the dimensions of the devtools client console being updated to match.
- """
- server_to_client: WebSocketResponse = next(
- iter(server.app["service"].clients)
- ).websocket
- server_info = {
- "type": "server_info",
- "payload": {
- "width": 123,
- "height": 456,
- },
- }
- await server_to_client.send_json(server_info)
- await wait_for_predicate(
- lambda: devtools.console.size == ConsoleDimensions(123, 456)
- )
diff --git a/tests/devtools/test_redirect_output.py b/tests/devtools/test_redirect_output.py
deleted file mode 100644
index f3e253701..000000000
--- a/tests/devtools/test_redirect_output.py
+++ /dev/null
@@ -1,108 +0,0 @@
-from contextlib import redirect_stdout
-from datetime import datetime
-
-import msgpack
-import time_machine
-
-from textual.devtools.redirect_output import StdoutRedirector
-
-TIMESTAMP = 1649166819
-
-
-@time_machine.travel(datetime.utcfromtimestamp(TIMESTAMP))
-async def test_print_redirect_to_devtools_only(devtools):
- await devtools._stop_log_queue_processing()
-
- with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
- print("Hello, world!")
-
- assert devtools.log_queue.qsize() == 1
-
- queued_log = await devtools.log_queue.get()
- queued_log_data = msgpack.unpackb(queued_log)
- print(repr(queued_log_data))
- payload = queued_log_data["payload"]
-
- assert queued_log_data["type"] == "client_log"
- assert payload["timestamp"] == TIMESTAMP
- assert (
- payload["segments"]
- == b"\x80\x04\x95B\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x0crich.segment\x94\x8c\x07Segment\x94\x93\x94\x8c\rHello, world!\x94NN\x87\x94\x81\x94h\x03\x8c\x01\n\x94NN\x87\x94\x81\x94e."
- )
-
-
-async def test_print_redirect_to_logfile_only(devtools):
- await devtools.disconnect()
- with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
- print("Hello, world!")
-
-
-async def test_print_redirect_to_devtools_and_logfile(devtools):
- await devtools._stop_log_queue_processing()
-
- with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
- print("Hello, world!")
-
- assert devtools.log_queue.qsize() == 1
-
-
-@time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
-async def test_print_without_flush_not_sent_to_devtools(devtools):
- await devtools._stop_log_queue_processing()
-
- with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
- # End is no longer newline character, so print will no longer
- # flush the output buffer by default.
- print("Hello, world!", end="")
-
- assert devtools.log_queue.qsize() == 0
-
-
-@time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
-async def test_print_forced_flush_sent_to_devtools(devtools):
- await devtools._stop_log_queue_processing()
-
- with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
- print("Hello, world!", end="", flush=True)
-
- assert devtools.log_queue.qsize() == 1
-
-
-@time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
-async def test_print_multiple_args_batched_as_one_log(devtools):
- await devtools._stop_log_queue_processing()
- redirector = StdoutRedirector(devtools)
- with redirect_stdout(redirector): # type: ignore
- # This print adds 3 messages to the buffer that can be batched
- print("The first", "batch", "of logs", end="")
- # This message cannot be batched with the previous message,
- # and so it will be the 2nd item added to the log queue.
- print("I'm in the second batch")
-
- assert devtools.log_queue.qsize() == 2
-
-
-@time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
-async def test_print_strings_containing_newline_flushed(devtools):
- await devtools._stop_log_queue_processing()
-
- with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
- # Flushing is disabled since end="", but the first
- # string will be flushed since it contains a newline
- print("Hel\nlo", end="")
- print("world", end="")
-
- assert devtools.log_queue.qsize() == 1
-
-
-@time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
-async def test_flush_flushes_buffered_logs(devtools):
- await devtools._stop_log_queue_processing()
-
- redirector = StdoutRedirector(devtools)
- with redirect_stdout(redirector): # type: ignore
- print("x", end="")
-
- assert devtools.log_queue.qsize() == 0
- redirector.flush()
- assert devtools.log_queue.qsize() == 1
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index 235a2c7ed..466adaa06 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -1028,170 +1028,6 @@
'''
# ---
-# name: test_borders_preview
- '''
-
-
- '''
-# ---
# name: test_buttons_render
'''