This commit is contained in:
Will McGugan
2022-02-24 16:02:23 +00:00
parent d83c0090b1
commit 57a05c7bbd
10 changed files with 903 additions and 752 deletions

233
poetry.lock generated
View File

@@ -72,7 +72,7 @@ python-versions = ">=3.6.1"
[[package]]
name = "click"
version = "8.0.3"
version = "8.0.4"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
@@ -103,7 +103,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
[[package]]
name = "coverage"
version = "6.3.1"
version = "6.3.2"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
@@ -148,7 +148,7 @@ dev = ["twine", "markdown", "flake8", "wheel"]
[[package]]
name = "identify"
version = "2.4.10"
version = "2.4.11"
description = "File identification library for Python"
category = "dev"
optional = false
@@ -304,21 +304,21 @@ pytkdocs = ">=0.14.0"
[[package]]
name = "mypy"
version = "0.910"
version = "0.931"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.6"
[package.dependencies]
mypy-extensions = ">=0.4.3,<0.5.0"
toml = "*"
typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""}
typing-extensions = ">=3.7.4"
mypy-extensions = ">=0.4.3"
tomli = ">=1.1.0"
typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
typing-extensions = ">=3.10"
[package.extras]
dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<1.5.0)"]
python2 = ["typed-ast (>=1.4.0,<2)"]
[[package]]
name = "mypy-extensions"
@@ -357,7 +357,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "platformdirs"
version = "2.5.0"
version = "2.5.1"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
@@ -488,11 +488,11 @@ six = ">=1.5"
[[package]]
name = "pytkdocs"
version = "0.15.0"
version = "0.16.0"
description = "Load Python objects documentation."
category = "dev"
optional = false
python-versions = ">=3.6.2"
python-versions = ">=3.7"
[package.dependencies]
astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""}
@@ -564,11 +564,11 @@ python-versions = ">=3.7"
[[package]]
name = "typed-ast"
version = "1.4.3"
version = "1.5.2"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = "*"
python-versions = ">=3.6"
[[package]]
name = "typing-extensions"
@@ -580,7 +580,7 @@ python-versions = "*"
[[package]]
name = "virtualenv"
version = "20.13.1"
version = "20.13.2"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
@@ -623,7 +623,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "3e7523fa7dbaa4f8c78acc4189fae9bee5b5ecdcca4d437027c7068283634121"
content-hash = "017cd9bd80c2432f3de493d190c162e98894d9bb1db0482502658e2bc9231887"
[metadata.files]
astunparse = [
@@ -672,8 +672,8 @@ cfgv = [
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
click = [
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
{file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
{file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
{file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@@ -684,47 +684,47 @@ commonmark = [
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
]
coverage = [
{file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"},
{file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"},
{file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"},
{file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"},
{file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"},
{file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"},
{file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"},
{file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"},
{file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"},
{file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"},
{file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"},
{file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"},
{file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"},
{file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"},
{file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"},
{file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"},
{file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"},
{file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"},
{file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"},
{file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"},
{file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"},
{file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"},
{file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"},
{file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"},
{file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"},
{file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"},
{file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"},
{file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"},
{file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"},
{file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"},
{file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"},
{file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"},
{file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"},
{file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"},
{file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"},
{file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"},
{file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"},
{file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"},
{file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"},
{file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"},
{file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"},
{file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"},
{file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"},
{file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"},
{file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"},
{file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"},
{file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"},
{file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"},
{file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"},
{file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"},
{file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"},
{file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"},
{file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"},
{file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"},
{file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"},
{file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"},
{file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"},
{file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"},
{file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"},
{file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"},
{file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"},
{file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"},
{file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"},
{file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"},
{file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"},
{file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"},
{file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"},
{file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"},
{file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"},
{file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"},
{file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"},
{file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"},
{file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"},
{file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"},
{file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"},
{file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"},
{file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"},
{file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"},
{file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"},
{file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"},
{file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
{file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
]
distlib = [
{file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
@@ -739,8 +739,8 @@ ghp-import = [
{file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"},
]
identify = [
{file = "identify-2.4.10-py2.py3-none-any.whl", hash = "sha256:7d10baf6ba6f1912a0a49f4c1c2c49fa1718765c3a37d72d13b07779567c5b85"},
{file = "identify-2.4.10.tar.gz", hash = "sha256:e12b2aea3cf108de73ae055c2260783bde6601de09718f6768cf8e9f6f6322a6"},
{file = "identify-2.4.11-py2.py3-none-any.whl", hash = "sha256:fd906823ed1db23c7a48f9b176a1d71cb8abede1e21ebe614bac7bdd688d9213"},
{file = "identify-2.4.11.tar.gz", hash = "sha256:2986942d3974c8f2e5019a190523b0b0e2a07cb8e89bf236727fb4b26f27f8fd"},
]
importlib-metadata = [
{file = "importlib_metadata-4.11.1-py3-none-any.whl", hash = "sha256:e0bc84ff355328a4adfc5240c4f211e0ab386f80aa640d1b11f0618a1d282094"},
@@ -825,29 +825,26 @@ mkdocstrings = [
{file = "mkdocstrings-0.17.0.tar.gz", hash = "sha256:75b5cfa2039aeaf3a5f5cf0aa438507b0330ce76c8478da149d692daa7213a98"},
]
mypy = [
{file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"},
{file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"},
{file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"},
{file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"},
{file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"},
{file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"},
{file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"},
{file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"},
{file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"},
{file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"},
{file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"},
{file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"},
{file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"},
{file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"},
{file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"},
{file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"},
{file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"},
{file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"},
{file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"},
{file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"},
{file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"},
{file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"},
{file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"},
{file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"},
{file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"},
{file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"},
{file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"},
{file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"},
{file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"},
{file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"},
{file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"},
{file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"},
{file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"},
{file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"},
{file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"},
{file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"},
{file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"},
{file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"},
{file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"},
{file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"},
{file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"},
{file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"},
{file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
@@ -866,8 +863,8 @@ pathspec = [
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
platformdirs = [
{file = "platformdirs-2.5.0-py3-none-any.whl", hash = "sha256:30671902352e97b1eafd74ade8e4a694782bd3471685e78c32d0fdfd3aa7e7bb"},
{file = "platformdirs-2.5.0.tar.gz", hash = "sha256:8ec11dfba28ecc0715eb5fb0147a87b1bf325f349f3da9aab2cd6b50b96b692b"},
{file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"},
{file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
@@ -906,8 +903,8 @@ python-dateutil = [
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
pytkdocs = [
{file = "pytkdocs-0.15.0-py3-none-any.whl", hash = "sha256:d6b2aec34448ec89acb8c1c25062cc1e70c6b26395d46fc7ee753b7e5a4e736a"},
{file = "pytkdocs-0.15.0.tar.gz", hash = "sha256:4b45af89d6fa5fa50f979b0f9f54539286b84e245c81991bb838149aa2d9d9c9"},
{file = "pytkdocs-0.16.0-py3-none-any.whl", hash = "sha256:098405ff347797cfb887a4d78f161372109bfbfffa0f8ee8daa5e2b8a8caba4b"},
{file = "pytkdocs-0.16.0.tar.gz", hash = "sha256:274407bcefec58d7e411adb03b84da49120107912b1697d17d4a3aea0583f388"},
]
pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
@@ -965,36 +962,30 @@ tomli = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
typed-ast = [
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
{file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
{file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
{file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
{file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
{file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
{file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
{file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
{file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
{file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
{file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
{file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
{file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
{file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
{file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"},
{file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"},
{file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"},
{file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"},
{file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"},
{file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"},
{file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"},
{file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"},
{file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"},
{file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"},
{file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"},
{file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"},
{file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"},
{file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"},
{file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"},
{file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"},
{file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"},
{file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"},
{file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"},
{file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"},
{file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"},
{file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"},
{file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"},
{file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
@@ -1002,8 +993,8 @@ typing-extensions = [
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
virtualenv = [
{file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"},
{file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"},
{file = "virtualenv-20.13.2-py2.py3-none-any.whl", hash = "sha256:e7b34c9474e6476ee208c43a4d9ac1510b041c68347eabfe9a9ea0c86aa0a46b"},
{file = "virtualenv-20.13.2.tar.gz", hash = "sha256:01f5f80744d24a3743ce61858123488e91cb2dd1d3bdf92adaf1bba39ffdedf0"},
]
watchdog = [
{file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"},

View File

@@ -27,7 +27,7 @@ typing-extensions = { version = "^3.10.0", python = "<3.8" }
[tool.poetry.dev-dependencies]
pytest = "^6.2.3"
black = "^22.1.0"
mypy = "^0.910"
mypy = "^0.931"
pytest-cov = "^2.12.1"
mkdocs = "^1.2.3"
mkdocstrings = "^0.17.0"

477
src/textual/_arrangement.py Normal file
View File

@@ -0,0 +1,477 @@
from __future__ import annotations
from itertools import chain
from operator import attrgetter, itemgetter
import sys
from typing import Iterator, Iterable, NamedTuple, TYPE_CHECKING
import rich.repr
from rich.console import Console, ConsoleOptions, RenderResult
from rich.control import Control
from rich.segment import Segment, SegmentLines
from rich.style import Style
from . import log
from .geometry import Region, Offset, Size
from .layout import WidgetPlacement
from ._loop import loop_last
from ._types import Lines
if sys.version_info >= (3, 10):
from typing import TypeAlias
else: # pragma: no cover
from typing_extensions import TypeAlias
if TYPE_CHECKING:
from .widget import Widget
class NoWidget(Exception):
"""Raised when there is no widget at the requested coordinate."""
class ReflowResult(NamedTuple):
"""The result of a reflow operation. Describes the chances to widgets."""
hidden: set[Widget]
shown: set[Widget]
resized: set[Widget]
class RenderRegion(NamedTuple):
"""Defines the absolute location of a Widget."""
region: Region
order: tuple[int, ...]
clip: Region
RenderRegionMap: TypeAlias = dict[Widget, RenderRegion]
@rich.repr.auto
class LayoutUpdate:
"""A renderable containing the result of a render for a given region."""
def __init__(self, lines: Lines, region: Region) -> None:
self.lines = lines
self.region = region
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
yield Control.home()
x = self.region.x
new_line = Segment.line()
move_to = Control.move_to
for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)):
yield move_to(x, y)
yield from line
if not last:
yield new_line
def __rich_repr__(self) -> rich.repr.Result:
x, y, width, height = self.region
yield "x", x
yield "y", y
yield "width", width
yield "height", height
@rich.repr.auto(angular=True)
class Arrangement:
"""Responsible for storing information regarding the relative positions of Widgets and rendering them."""
def __init__(self) -> None:
# A mapping of Widget on to its "render location" (absolute position / depth)
self.map: RenderRegionMap = {}
# All widgets considered in the arrangement
# Not this may be a supperset of self.map.keys() as some widgets may be invisible for various reasons
self.widgets: set[Widget] = set()
# Dimensions of the arrangement
self.width = 0
self.height = 0
self.regions: dict[Widget, tuple[Region, Region]] = {}
self._cuts: list[list[int]] | None = None
self._require_update: bool = True
self.background = ""
def __rich_repr__(self) -> rich.repr.Result:
yield "width", self.width
yield "height", self.height
yield "widgets", self.widgets
def check_update(self) -> bool:
return self._require_update
def require_update(self) -> None:
self._require_update = True
self.reset()
self.map.clear()
self.widgets.clear()
def reset_update(self) -> None:
self._require_update = False
def reset(self) -> None:
self._cuts = None
def reflow(self, parent: Widget, size: Size) -> ReflowResult:
"""Reflow (layout) widget and its children.
Args:
parent (Widget): The root widget.
size (Size): Size of the area to be filled.
Returns:
ReflowResult: Hidden shown and resized widgets
"""
self.reset()
self.width = size.width
self.height = size.height
map, virtual_size = self._arrange_root(parent)
self._require_update = False
old_widgets = set(self.map.keys())
new_widgets = set(map.keys())
# Newly visible widgets
shown_widgets = new_widgets - old_widgets
# Newly hidden widgets
hidden_widgets = old_widgets - new_widgets
self._layout_map = map
# Copy renders if the size hasn't changed
new_renders = {
widget: (region, clip) for widget, (region, _order, clip) in map.items()
}
self.regions = new_renders
# Widgets with changed size
resized_widgets = {
widget
for widget, (region, *_) in map.items()
if widget in old_widgets and widget.size != region.size
}
parent.virtual_size = virtual_size
return ReflowResult(
hidden=hidden_widgets, shown=shown_widgets, resized=resized_widgets
)
def _arrange_root(self, root: Widget) -> tuple[RenderRegionMap, Size]:
"""Arrange a widgets children based on its layout attribute.
Args:
root (Widget): Top level widget.
Returns:
map[dict[Widget, RenderRegion], Size]: A mapping of widget on to render region
and the "virtual size" (scrollable reason)
"""
size = Size(self.width, self.height)
ORIGIN = Offset(0, 0)
map: dict[Widget, RenderRegion] = {}
def add_widget(
widget,
region: Region,
order: tuple[int, ...],
clip: Region,
):
widgets: set[Widget] = set()
styles_offset = widget.styles.offset
total_region = region
layout_offset = (
styles_offset.resolve(region.size, clip.size)
if styles_offset
else ORIGIN
)
map[widget] = RenderRegion(region + layout_offset, order, clip)
if widget.layout is not None:
scroll = widget.scroll
total_region = region.size.region
sub_clip = clip.intersection(region)
placements: list[WidgetPlacement] = []
add_placement = placements.append
iter_arrange = iter(widget.layout.arrange(widget, region.size, scroll))
try:
while True:
add_placement(iter_arrange.__next__())
except StopIteration as stop_iteration:
widgets.update(stop_iteration.value)
placements = sorted(
[
placement.apply_margin()
for placement in widget.layout.arrange(
widget, region.size, scroll
)
],
key=attrgetter("order"),
)
for sub_region, sub_widget, z in placements:
total_region = total_region.union(sub_region)
if sub_widget is not None:
add_widget(
sub_widget,
sub_region + region.origin - scroll,
sub_widget.z + (z,),
sub_clip,
)
return total_region.size
virtual_size = add_widget(root, size.region, (), size.region)
return map, virtual_size
async def mount_all(self, view: "View") -> None:
view.mount(*self.widgets)
def __iter__(self) -> Iterator[tuple[Widget, Region, Region]]:
layers = sorted(self.map.items(), key=lambda item: item[1].order, reverse=True)
for widget, (region, order, clip) in layers:
yield widget, region.intersection(clip), region
def get_offset(self, widget: Widget) -> Offset:
"""Get the offset of a widget."""
try:
return self.map[widget].region.origin
except KeyError:
raise NoWidget("Widget is not in layout")
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
"""Get the widget under the given point or None."""
for widget, cropped_region, region in self:
if widget.is_visual and cropped_region.contains(x, y):
return widget, region
raise NoWidget(f"No widget under screen coordinate ({x}, {y})")
def get_style_at(self, x: int, y: int) -> Style:
"""Get the Style at the given cell or Style.null()
Args:
x (int): X position within the Layout
y (int): Y position within the Layout
Returns:
Style: The Style at the cell (x, y) within the Layout
"""
try:
widget, region = self.get_widget_at(x, y)
except NoWidget:
return Style.null()
if widget not in self.regions:
return Style.null()
lines = widget._get_lines()
x -= region.x
y -= region.y
line = lines[y]
end = 0
for segment in line:
end += segment.cell_length
if x < end:
return segment.style or Style.null()
return Style.null()
def get_widget_region(self, widget: Widget) -> Region:
"""Get the Region of a Widget contained in this Layout.
Args:
widget (Widget): The Widget in this layout you wish to know the Region of.
Raises:
NoWidget: If the Widget is not contained in this Layout.
Returns:
Region: The Region of the Widget.
"""
try:
region, *_ = self.map[widget]
except KeyError:
raise NoWidget("Widget is not in layout")
else:
return region
@property
def cuts(self) -> list[list[int]]:
"""Get vertical cuts.
A cut is every point on a line where a widget starts or ends.
Returns:
list[list[int]]: A list of cuts for every line.
"""
if self._cuts is not None:
return self._cuts
width = self.width
height = self.height
screen_region = Region(0, 0, width, height)
cuts_sets = [{0, width} for _ in range(height)]
if self.map is not None:
for region, order, clip in self.map.values():
region = region.intersection(clip)
if region and (region in screen_region):
region_cuts = region.x_extents
for y in region.y_range:
cuts_sets[y].update(region_cuts)
# Sort the cuts for each line
self._cuts = [sorted(cut_set) for cut_set in cuts_sets]
return self._cuts
def _get_renders(self, console: Console) -> Iterable[tuple[Region, Region, Lines]]:
_rich_traceback_guard = True
layout_map = self.map
if layout_map:
widget_regions = sorted(
(
(widget, region, order, clip)
for widget, (region, order, clip) in layout_map.items()
),
key=itemgetter(2),
reverse=True,
)
else:
widget_regions = []
for widget, region, _order, clip in widget_regions:
if not (widget.is_visual and widget.visible):
continue
lines = widget._get_lines()
if region in clip:
yield region, clip, lines
elif clip.overlaps(region):
new_region = region.intersection(clip)
delta_x = new_region.x - region.x
delta_y = new_region.y - region.y
splits = [delta_x, delta_x + new_region.width]
lines = lines[delta_y : delta_y + new_region.height]
divide = Segment.divide
lines = [list(divide(line, splits))[1] for line in lines]
yield region, clip, lines
@classmethod
def _assemble_chops(
cls, chops: list[dict[int, list[Segment] | None]]
) -> Iterable[Iterable[Segment]]:
from_iterable = chain.from_iterable
for bucket in chops:
yield from_iterable(
line for _, line in sorted(bucket.items()) if line is not None
)
def render(
self,
console: Console,
*,
crop: Region = None,
) -> SegmentLines:
"""Render a layout.
Args:
console (Console): Console instance.
clip (Optional[Region]): Region to clip to.
Returns:
SegmentLines: A renderable
"""
width = self.width
height = self.height
screen = Region(0, 0, width, height)
crop_region = crop.intersection(screen) if crop else screen
_Segment = Segment
divide = _Segment.divide
# Maps each cut on to a list of segments
cuts = self.cuts
chops: list[dict[int, list[Segment] | None]] = [
{cut: None for cut in cut_set} for cut_set in cuts
]
# TODO: Provide an option to update the background
background_style = console.get_style(self.background)
background_render = [
[_Segment(" " * width, background_style)] for _ in range(height)
]
# Go through all the renders in reverse order and fill buckets with no render
renders = list(self._get_renders(console))
for region, clip, lines in chain(
renders, [(screen, screen, background_render)]
):
render_region = region.intersection(clip)
for y, line in zip(render_region.y_range, lines):
first_cut, last_cut = render_region.x_extents
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
if len(final_cuts) == 2:
cut_segments = [line]
else:
render_x = render_region.x
relative_cuts = [cut - render_x for cut in final_cuts]
_, *cut_segments = divide(line, relative_cuts)
for cut, segments in zip(final_cuts, cut_segments):
if chops[y][cut] is None:
chops[y][cut] = segments
# Assemble the cut renders in to lists of segments
crop_x, crop_y, crop_x2, crop_y2 = crop_region.corners
output_lines = self._assemble_chops(chops[crop_y:crop_y2])
def width_view(line: list[Segment]) -> list[Segment]:
if line:
div_lines = list(divide(line, [crop_x, crop_x2]))
line = div_lines[1] if len(div_lines) > 1 else div_lines[0]
return line
if crop is not None and (crop_x, crop_x2) != (0, self.width):
render_lines = [width_view(line) for line in output_lines]
else:
render_lines = list(output_lines)
return SegmentLines(render_lines, new_lines=True)
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
yield self.render(console)
def update_widget(self, console: Console, widget: Widget) -> LayoutUpdate | None:
if widget not in self.regions:
return None
region, clip = self.regions[widget]
if not region.size:
return None
widget.clear_render_cache()
update_region = region.intersection(clip)
update_lines = self.render(console, crop=update_region).lines
update = LayoutUpdate(update_lines, update_region)
log(update)
return update

View File

@@ -662,7 +662,7 @@ class App(DOMNode):
async def handle_layout(self, message: messages.Layout) -> None:
message.stop()
await self.view.refresh_layout()
# await self.view.refresh_layout()
self.app.refresh()
async def on_key(self, event: events.Key) -> None:

View File

@@ -1,24 +1,10 @@
from __future__ import annotations
import sys
from abc import ABC, abstractmethod
from itertools import chain
from operator import itemgetter
from typing import ClassVar, Iterable, Iterator, NamedTuple, TYPE_CHECKING
from typing import ClassVar, Generator, Iterable, NamedTuple, TYPE_CHECKING
import rich.repr
from rich.console import Console, ConsoleOptions, RenderResult
from rich.control import Control
from rich.segment import Segment, SegmentLines
from rich.style import Style
from . import log
from ._loop import loop_last
from ._types import Lines
from .geometry import Region, Offset, Size
from .layout_map import LayoutMap
PY38 = sys.version_info >= (3, 8)
if TYPE_CHECKING:
@@ -26,18 +12,6 @@ if TYPE_CHECKING:
from .view import View
class NoWidget(Exception):
"""Raised when there is no widget at the requested coordinate."""
class ReflowResult(NamedTuple):
"""The result of a reflow operation. Describes the chances to widgets."""
hidden: set[Widget]
shown: set[Widget]
resized: set[Widget]
class WidgetPlacement(NamedTuple):
"""The position, size, and relative order of a widget within its parent."""
@@ -66,364 +40,22 @@ class WidgetPlacement(NamedTuple):
return self
@rich.repr.auto
class LayoutUpdate:
"""A renderable containing the result of a render for a given region."""
def __init__(self, lines: Lines, region: Region) -> None:
self.lines = lines
self.region = region
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
yield Control.home()
x = self.region.x
new_line = Segment.line()
move_to = Control.move_to
for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)):
yield move_to(x, y)
yield from line
if not last:
yield new_line
def __rich_repr__(self) -> rich.repr.Result:
x, y, width, height = self.region
yield "x", x
yield "y", y
yield "width", width
yield "height", height
class Layout(ABC):
"""Responsible for arranging Widgets in a view and rendering them."""
name: ClassVar[str] = ""
def __init__(self) -> None:
self._layout_map: LayoutMap | None = None
self.width = 0
self.height = 0
self.regions: dict[Widget, tuple[Region, Region]] = {}
self._cuts: list[list[int]] | None = None
self._require_update: bool = True
self.background = ""
def check_update(self) -> bool:
return self._require_update
def require_update(self) -> None:
self._require_update = True
self.reset()
self._layout_map = None
def reset_update(self) -> None:
self._require_update = False
def reset(self) -> None:
self._cuts = None
def reflow(self, view: View, size: Size) -> ReflowResult:
self.reset()
self.width = size.width
self.height = size.height
map = LayoutMap(size)
map.add_widget(view, size.region, (), size.region)
self._require_update = False
old_widgets = set() if self.map is None else set(self.map.keys())
new_widgets = set(map.keys())
# Newly visible widgets
shown_widgets = new_widgets - old_widgets
# Newly hidden widgets
hidden_widgets = old_widgets - new_widgets
self._layout_map = map
# Copy renders if the size hasn't changed
new_renders = {
widget: (region, clip) for widget, (region, _order, clip) in map.items()
}
self.regions = new_renders
# Widgets with changed size
resized_widgets = {
widget
for widget, (region, *_) in map.items()
if widget in old_widgets and widget.size != region.size
}
return ReflowResult(
hidden=hidden_widgets, shown=shown_widgets, resized=resized_widgets
)
@abstractmethod
def get_widgets(self, view: View) -> Iterable[Widget]:
...
@abstractmethod
def arrange(
self, view: View, size: Size, scroll: Offset
) -> Iterable[WidgetPlacement]:
self, parent: View, size: Size, scroll: Offset
) -> Generator[WidgetPlacement, None, set[Widget]]:
"""Generate a layout map that defines where on the screen the widgets will be drawn.
Args:
view (View): The View instance.
parent (Widget): Parent widget.
size (Size): Size of container.
scroll (Offset): Offset to apply to the Widget placements.
Returns:
Iterable[WidgetPlacement]: An iterable of widget location
"""
async def mount_all(self, view: "View") -> None:
widgets = list(self.get_widgets(view))
if widgets:
view.mount(*widgets)
@property
def map(self) -> LayoutMap:
assert self._layout_map is not None
return self._layout_map
def __iter__(self) -> Iterator[tuple[Widget, Region, Region]]:
if self.map is not None:
layers = sorted(
self.map.widgets.items(), key=lambda item: item[1].order, reverse=True
)
for widget, (region, order, clip) in layers:
yield widget, region.intersection(clip), region
def get_offset(self, widget: Widget) -> Offset:
"""Get the offset of a widget."""
try:
return self.map[widget].region.origin
except KeyError:
raise NoWidget("Widget is not in layout")
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
"""Get the widget under the given point or None."""
for widget, cropped_region, region in self:
if widget.is_visual and cropped_region.contains(x, y):
return widget, region
raise NoWidget(f"No widget under screen coordinate ({x}, {y})")
def get_style_at(self, x: int, y: int) -> Style:
"""Get the Style at the given cell or Style.null()
Args:
x (int): X position within the Layout
y (int): Y position within the Layout
Returns:
Style: The Style at the cell (x, y) within the Layout
"""
try:
widget, region = self.get_widget_at(x, y)
except NoWidget:
return Style.null()
if widget not in self.regions:
return Style.null()
lines = widget._get_lines()
x -= region.x
y -= region.y
line = lines[y]
end = 0
for segment in line:
end += segment.cell_length
if x < end:
return segment.style or Style.null()
return Style.null()
def get_widget_region(self, widget: Widget) -> Region:
"""Get the Region of a Widget contained in this Layout.
Args:
widget (Widget): The Widget in this layout you wish to know the Region of.
Raises:
NoWidget: If the Widget is not contained in this Layout.
Returns:
Region: The Region of the Widget.
"""
try:
region, *_ = self.map[widget]
except KeyError:
raise NoWidget("Widget is not in layout")
else:
return region
@property
def cuts(self) -> list[list[int]]:
"""Get vertical cuts.
A cut is every point on a line where a widget starts or ends.
Returns:
list[list[int]]: A list of cuts for every line.
"""
if self._cuts is not None:
return self._cuts
width = self.width
height = self.height
screen_region = Region(0, 0, width, height)
cuts_sets = [{0, width} for _ in range(height)]
if self.map is not None:
for region, order, clip in self.map.values():
region = region.intersection(clip)
if region and (region in screen_region):
region_cuts = region.x_extents
for y in region.y_range:
cuts_sets[y].update(region_cuts)
# Sort the cuts for each line
self._cuts = [sorted(cut_set) for cut_set in cuts_sets]
return self._cuts
def _get_renders(self, console: Console) -> Iterable[tuple[Region, Region, Lines]]:
_rich_traceback_guard = True
layout_map = self.map
if layout_map:
widget_regions = sorted(
(
(widget, region, order, clip)
for widget, (region, order, clip) in layout_map.items()
),
key=itemgetter(2),
reverse=True,
)
else:
widget_regions = []
for widget, region, _order, clip in widget_regions:
if not (widget.is_visual and widget.visible):
continue
lines = widget._get_lines()
if region in clip:
yield region, clip, lines
elif clip.overlaps(region):
new_region = region.intersection(clip)
delta_x = new_region.x - region.x
delta_y = new_region.y - region.y
splits = [delta_x, delta_x + new_region.width]
lines = lines[delta_y : delta_y + new_region.height]
divide = Segment.divide
lines = [list(divide(line, splits))[1] for line in lines]
yield region, clip, lines
@classmethod
def _assemble_chops(
cls, chops: list[dict[int, list[Segment] | None]]
) -> Iterable[Iterable[Segment]]:
from_iterable = chain.from_iterable
for bucket in chops:
yield from_iterable(
line for _, line in sorted(bucket.items()) if line is not None
)
def render(
self,
console: Console,
*,
crop: Region = None,
) -> SegmentLines:
"""Render a layout.
Args:
console (Console): Console instance.
clip (Optional[Region]): Region to clip to.
Returns:
SegmentLines: A renderable
"""
width = self.width
height = self.height
screen = Region(0, 0, width, height)
crop_region = crop.intersection(screen) if crop else screen
_Segment = Segment
divide = _Segment.divide
# Maps each cut on to a list of segments
cuts = self.cuts
chops: list[dict[int, list[Segment] | None]] = [
{cut: None for cut in cut_set} for cut_set in cuts
]
# TODO: Provide an option to update the background
background_style = console.get_style(self.background)
background_render = [
[_Segment(" " * width, background_style)] for _ in range(height)
]
# Go through all the renders in reverse order and fill buckets with no render
renders = list(self._get_renders(console))
for region, clip, lines in chain(
renders, [(screen, screen, background_render)]
):
render_region = region.intersection(clip)
for y, line in zip(render_region.y_range, lines):
first_cut, last_cut = render_region.x_extents
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
if len(final_cuts) == 2:
cut_segments = [line]
else:
render_x = render_region.x
relative_cuts = [cut - render_x for cut in final_cuts]
_, *cut_segments = divide(line, relative_cuts)
for cut, segments in zip(final_cuts, cut_segments):
if chops[y][cut] is None:
chops[y][cut] = segments
# Assemble the cut renders in to lists of segments
crop_x, crop_y, crop_x2, crop_y2 = crop_region.corners
output_lines = self._assemble_chops(chops[crop_y:crop_y2])
def width_view(line: list[Segment]) -> list[Segment]:
if line:
div_lines = list(divide(line, [crop_x, crop_x2]))
line = div_lines[1] if len(div_lines) > 1 else div_lines[0]
return line
if crop is not None and (crop_x, crop_x2) != (0, self.width):
render_lines = [width_view(line) for line in output_lines]
else:
render_lines = list(output_lines)
return SegmentLines(render_lines, new_lines=True)
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
yield self.render(console)
def update_widget(self, console: Console, widget: Widget) -> LayoutUpdate | None:
if widget not in self.regions:
return None
region, clip = self.regions[widget]
if not region.size:
return None
widget.clear_render_cache()
update_region = region.intersection(clip)
update_lines = self.render(console, crop=update_region).lines
update = LayoutUpdate(update_lines, update_region)
log(update)
return update

View File

@@ -1,3 +1,9 @@
"""
Planned for deprecation
"""
from __future__ import annotations
@@ -10,12 +16,16 @@ from .widget import Widget
class RenderRegion(NamedTuple):
"""Defines the absolute location of a Widget."""
region: Region
order: tuple[int, ...]
clip: Region
class LayoutMap:
"""A container that maps widgets on to their absolute location."""
def __init__(self, size: Size) -> None:
self.size = size
self.widgets: dict[Widget, RenderRegion] = {}
@@ -53,6 +63,8 @@ class LayoutMap:
self.widgets[widget] = RenderRegion(region + layout_offset, order, clip)
# TODO: replace with widget.layout
if isinstance(widget, View):
view: View = widget
scroll = view.scroll

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import sys
from collections import defaultdict
from dataclasses import dataclass
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
from typing import Generator, Iterable, TYPE_CHECKING, NamedTuple, Sequence
from .._layout_resolve import layout_resolve
from ..css.types import Edge
@@ -47,31 +47,27 @@ class DockLayout(Layout):
def __repr__(self):
return "<DockLayout>"
def get_docks(self, view: View) -> list[Dock]:
def get_docks(self, parent: Widget) -> list[Dock]:
groups: dict[str, list[Widget]] = defaultdict(list)
for child in view.children:
for child in parent.children:
assert isinstance(child, Widget)
if child.display:
groups[child.styles.dock].append(child)
docks: list[Dock] = []
append_dock = docks.append
for name, edge, z in view.styles.docks:
for name, edge, z in parent.styles.docks:
append_dock(Dock(edge, groups[name], z))
return docks
def get_widgets(self, view: View) -> Iterable[Widget]:
for dock in self.get_docks(view):
yield from dock.widgets
def arrange(
self, view: View, size: Size, scroll: Offset
) -> Iterable[WidgetPlacement]:
self, parent: Widget, size: Size, scroll: Offset
) -> Generator[WidgetPlacement, None, set[Widget]]:
width, height = size
layout_region = Region(0, 0, width, height)
layers: dict[int, Region] = defaultdict(lambda: layout_region)
docks = self.get_docks(view)
docks = self.get_docks(parent)
def make_dock_options(widget, edge: Edge) -> DockOptions:
styles = widget.styles
@@ -93,11 +89,14 @@ class DockLayout(Layout):
Placement = WidgetPlacement
arranged_widgets: set[Widget] = set()
for edge, widgets, z in docks:
arranged_widgets.update(widgets)
dock_options = [make_dock_options(widget, edge) for widget in widgets]
region = layers[z]
if not region:
if not region.area:
# No space left
continue
@@ -168,3 +167,5 @@ class DockLayout(Layout):
region = Region(x, y, width - total, height)
layers[z] = region
return arranged_widgets

View File

@@ -1,250 +1,17 @@
from __future__ import annotations
from typing import Callable, Iterable
from .widget import Widget
import rich.repr
from rich.console import RenderableType
from rich.style import Style
from . import errors, events, messages
from .geometry import Size, Offset, Region
from .layout import Layout, NoWidget, WidgetPlacement
from .reactive import Reactive, watch
from .widget import Widget
@rich.repr.auto
class View(Widget):
"""A widget for the root of the app."""
DEFAULT_STYLES = """
layout: dock;
layout: dock
docks: _default=top;
"""
def __init__(self, name: str | None = None, id: str | None = None) -> None:
self.mouse_over: Widget | None = None
self._mouse_style: Style = Style()
self._mouse_widget: Widget | None = None
self._cached_arrangement: tuple[Size, Offset, list[WidgetPlacement]] = (
Size(),
Offset(),
[],
)
super().__init__(name=name, id=id)
background: Reactive[str] = Reactive("")
scroll_x: Reactive[int] = Reactive(0)
scroll_y: Reactive[int] = Reactive(0)
virtual_size = Reactive(Size(0, 0))
async def watch_background(self, value: str) -> None:
self.layout.background = value
self.app.refresh()
@property
def layout(self) -> Layout | None:
"""Convenience property for accessing ``self.styles.layout``.
Returns: The Layout associated with this view
"""
return self.styles.layout
@layout.setter
def layout(self, new_value: Layout) -> None:
"""Convenience property setter for setting ``view.styles.layout``.
Args:
new_value:
Returns:
None
"""
self.styles.layout = new_value
@property
def scroll(self) -> Offset:
return Offset(self.scroll_x, self.scroll_y)
def __rich_repr__(self) -> rich.repr.Result:
yield "name", self.name
@property
def is_visual(self) -> bool:
return False
@property
def is_root_view(self) -> bool:
return bool(self._parent and self.parent is self.app)
def is_mounted(self, widget: Widget) -> bool:
return self.app.is_mounted(widget)
def render(self) -> RenderableType:
return self.layout or ""
def get_offset(self, widget: Widget) -> Offset:
return self.layout.get_offset(widget)
def get_arrangement(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
cached_size, cached_scroll, arrangement = self._cached_arrangement
if cached_size == size and cached_scroll == scroll:
return arrangement
placements = [
placement.apply_margin()
for placement in self.layout.arrange(self, size, scroll)
]
self._cached_arrangement = (size, scroll, placements)
return placements
async def handle_update(self, message: messages.Update) -> None:
if self.is_root_view:
message.stop()
widget = message.widget
assert isinstance(widget, Widget)
display_update = self.layout.update_widget(self.console, widget)
if display_update is not None:
self.app.display(display_update)
async def handle_layout(self, message: messages.Layout) -> None:
await self.refresh_layout()
if self.is_root_view:
message.stop()
self.app.refresh()
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
self.app.register(self, *anon_widgets, **widgets)
self.refresh()
async def refresh_layout(self) -> None:
self._cached_arrangement = (Size(), Offset(), [])
try:
await self.layout.mount_all(self)
if not self.is_root_view:
await self.app.view.refresh_layout()
return
if not self.size:
return
hidden, shown, resized = self.layout.reflow(self, Size(*self.console.size))
assert self.layout.map is not None
for widget in hidden:
widget.post_message_no_wait(events.Hide(self))
for widget in shown:
widget.post_message_no_wait(events.Show(self))
send_resize = shown
send_resize.update(resized)
for widget, region, unclipped_region in self.layout:
widget._update_size(unclipped_region.size)
if widget in send_resize:
widget.post_message_no_wait(
events.Resize(self, unclipped_region.size)
)
except Exception:
self.app.panic()
async def on_resize(self, event: events.Resize) -> None:
self._update_size(event.size)
if self.is_root_view:
await self.refresh_layout()
self.app.refresh()
event.stop()
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
return self.layout.get_widget_at(x, y)
def get_style_at(self, x: int, y: int) -> Style:
return self.layout.get_style_at(x, y)
def get_widget_region(self, widget: Widget) -> Region:
return self.layout.get_widget_region(widget)
async def on_mount(self, event: events.Mount) -> None:
async def watch_background(value: str) -> None:
self.background = value
watch(self.app, "background", watch_background)
async def on_idle(self, event: events.Idle) -> None:
if self.layout is None:
return
if self.layout.check_update():
self.layout.reset_update()
await self.refresh_layout()
async def _on_mouse_move(self, event: events.MouseMove) -> None:
try:
if self.app.mouse_captured:
widget = self.app.mouse_captured
region = self.get_widget_region(widget)
else:
widget, region = self.get_widget_at(event.x, event.y)
except NoWidget:
await self.app.set_mouse_over(None)
else:
await self.app.set_mouse_over(widget)
await widget.forward_event(
events.MouseMove(
self,
event.x - region.x,
event.y - region.y,
event.delta_x,
event.delta_y,
event.button,
event.shift,
event.meta,
event.ctrl,
screen_x=event.screen_x,
screen_y=event.screen_y,
style=event.style,
)
)
async def forward_event(self, event: events.Event) -> None:
event.set_forwarded()
if isinstance(event, (events.Enter, events.Leave)):
await self.post_message(event)
elif isinstance(event, events.MouseMove):
event.style = self.get_style_at(event.screen_x, event.screen_y)
await self._on_mouse_move(event)
elif isinstance(event, events.MouseEvent):
try:
if self.app.mouse_captured:
widget = self.app.mouse_captured
region = self.get_widget_region(widget)
else:
widget, region = self.get_widget_at(event.x, event.y)
except NoWidget:
pass
else:
if isinstance(event, events.MouseDown) and widget.can_focus:
await self.app.set_focus(widget)
event.style = self.get_style_at(event.screen_x, event.screen_y)
await widget.forward_event(event.offset(-region.x, -region.y))
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
try:
widget, _region = self.get_widget_at(event.x, event.y)
except NoWidget:
return
scroll_widget = widget
if scroll_widget is not None:
await scroll_widget.forward_event(event)
else:
self.log("view.forwarded", event)
await self.post_message(event)
async def action_toggle(self, name: str) -> None:
widget = self[name]
widget.visible = not widget.display
await self.post_message(messages.Layout(self))

253
src/textual/viewX.py Normal file
View File

@@ -0,0 +1,253 @@
from __future__ import annotations
from typing import Callable, Iterable
import rich.repr
from rich.console import RenderableType
from rich.style import Style
from . import errors, events, messages
from ._arrangement import Arrangement
from .geometry import Size, Offset, Region
from .layout import Layout, NoWidget, WidgetPlacement
from .reactive import Reactive, watch
from .widget import Widget
@rich.repr.auto
class View(Widget):
DEFAULT_STYLES = """
layout: dock;
docks: _default=top;
"""
def __init__(self, name: str | None = None, id: str | None = None) -> None:
self.mouse_over: Widget | None = None
self._mouse_style: Style = Style()
self._mouse_widget: Widget | None = None
self._arrangement = Arrangement()
self._cached_arrangement: tuple[Size, Offset, list[WidgetPlacement]] = (
Size(),
Offset(),
[],
)
super().__init__(name=name, id=id)
background: Reactive[str] = Reactive("")
scroll_x: Reactive[int] = Reactive(0)
scroll_y: Reactive[int] = Reactive(0)
virtual_size = Reactive(Size(0, 0))
async def watch_background(self, value: str) -> None:
self._arrangement.background = value
self.app.refresh()
@property
def layout(self) -> Layout | None:
"""Convenience property for accessing ``self.styles.layout``.
Returns: The Layout associated with this view
"""
return self.styles.layout
@layout.setter
def layout(self, new_value: Layout) -> None:
"""Convenience property setter for setting ``view.styles.layout``.
Args:
new_value:
Returns:
None
"""
self.styles.layout = new_value
@property
def scroll(self) -> Offset:
return Offset(self.scroll_x, self.scroll_y)
def __rich_repr__(self) -> rich.repr.Result:
yield "name", self.name
@property
def is_visual(self) -> bool:
return False
@property
def is_root_view(self) -> bool:
return bool(self._parent and self.parent is self.app)
def is_mounted(self, widget: Widget) -> bool:
return self.app.is_mounted(widget)
def render(self) -> RenderableType:
return self._arrangement
def get_offset(self, widget: Widget) -> Offset:
return self._arrangement.get_offset(widget)
def get_arrangement(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
cached_size, cached_scroll, arrangement = self._cached_arrangement
if cached_size == size and cached_scroll == scroll:
return arrangement
placements = [
placement.apply_margin()
for placement in self.layout.arrange(self, size, scroll)
]
self._cached_arrangement = (size, scroll, placements)
return placements
async def handle_update(self, message: messages.Update) -> None:
if self.is_root_view:
message.stop()
widget = message.widget
assert isinstance(widget, Widget)
display_update = self.layout.update_widget(self.console, widget)
if display_update is not None:
self.app.display(display_update)
async def handle_layout(self, message: messages.Layout) -> None:
await self.refresh_layout()
if self.is_root_view:
message.stop()
self.app.refresh()
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
self.app.register(self, *anon_widgets, **widgets)
self.refresh()
async def refresh_layout(self) -> None:
self._cached_arrangement = (Size(), Offset(), [])
try:
await self.layout.mount_all(self)
if not self.is_root_view:
await self.app.view.refresh_layout()
return
if not self.size:
return
hidden, shown, resized = self.layout.reflow(self, Size(*self.console.size))
assert self.layout.map is not None
for widget in hidden:
widget.post_message_no_wait(events.Hide(self))
for widget in shown:
widget.post_message_no_wait(events.Show(self))
send_resize = shown
send_resize.update(resized)
for widget, region, unclipped_region in self.layout:
widget._update_size(unclipped_region.size)
if widget in send_resize:
widget.post_message_no_wait(
events.Resize(self, unclipped_region.size)
)
except Exception:
self.app.panic()
async def on_resize(self, event: events.Resize) -> None:
self._update_size(event.size)
if self.is_root_view:
await self.refresh_layout()
self.app.refresh()
event.stop()
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
return self.layout.get_widget_at(x, y)
def get_style_at(self, x: int, y: int) -> Style:
return self.layout.get_style_at(x, y)
def get_widget_region(self, widget: Widget) -> Region:
return self.layout.get_widget_region(widget)
async def on_mount(self, event: events.Mount) -> None:
async def watch_background(value: str) -> None:
self.background = value
watch(self.app, "background", watch_background)
async def on_idle(self, event: events.Idle) -> None:
if self.layout is None:
return
if self.layout.check_update():
self.layout.reset_update()
await self.refresh_layout()
async def _on_mouse_move(self, event: events.MouseMove) -> None:
try:
if self.app.mouse_captured:
widget = self.app.mouse_captured
region = self.get_widget_region(widget)
else:
widget, region = self.get_widget_at(event.x, event.y)
except NoWidget:
await self.app.set_mouse_over(None)
else:
await self.app.set_mouse_over(widget)
await widget.forward_event(
events.MouseMove(
self,
event.x - region.x,
event.y - region.y,
event.delta_x,
event.delta_y,
event.button,
event.shift,
event.meta,
event.ctrl,
screen_x=event.screen_x,
screen_y=event.screen_y,
style=event.style,
)
)
async def forward_event(self, event: events.Event) -> None:
event.set_forwarded()
if isinstance(event, (events.Enter, events.Leave)):
await self.post_message(event)
elif isinstance(event, events.MouseMove):
event.style = self.get_style_at(event.screen_x, event.screen_y)
await self._on_mouse_move(event)
elif isinstance(event, events.MouseEvent):
try:
if self.app.mouse_captured:
widget = self.app.mouse_captured
region = self.get_widget_region(widget)
else:
widget, region = self.get_widget_at(event.x, event.y)
except NoWidget:
pass
else:
if isinstance(event, events.MouseDown) and widget.can_focus:
await self.app.set_focus(widget)
event.style = self.get_style_at(event.screen_x, event.screen_y)
await widget.forward_event(event.offset(-region.x, -region.y))
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
try:
widget, _region = self.get_widget_at(event.x, event.y)
except NoWidget:
return
scroll_widget = widget
if scroll_widget is not None:
await scroll_widget.forward_event(event)
else:
self.log("view.forwarded", event)
await self.post_message(event)
async def action_toggle(self, name: str) -> None:
widget = self[name]
widget.visible = not widget.display
await self.post_message(messages.Layout(self))

View File

@@ -16,6 +16,7 @@ import rich.repr
from rich.align import Align
from rich.console import Console, RenderableType
from rich.padding import Padding
from rich.pretty import Pretty
from rich.style import Style
from rich.styled import Styled
from rich.text import Text
@@ -28,10 +29,11 @@ from ._callback import invoke
from ._context import active_app
from ._types import Lines
from .dom import DOMNode
from .geometry import Size, Spacing
from .geometry import Offset, Size
from .message import Message
from .messages import Layout, Update
from .reactive import watch
from . import messages
from .layout import Layout
from .reactive import Reactive, watch
from .renderables.opacity import Opacity
if TYPE_CHECKING:
@@ -79,6 +81,10 @@ class Widget(DOMNode):
super().__init__(name=name, id=id)
scroll_x = Reactive(0)
scroll_y = Reactive(0)
virtual_size = Reactive(Size(0, 0))
def __init_subclass__(cls, can_focus: bool = True) -> None:
super().__init_subclass__()
cls.can_focus = can_focus
@@ -145,6 +151,10 @@ class Widget(DOMNode):
def size(self) -> Size:
return self._size
@property
def scroll(self) -> Offset:
return Offset(self.scroll_x, self.scroll_y)
@property
def is_visual(self) -> bool:
return True
@@ -166,6 +176,10 @@ class Widget(DOMNode):
assert self._animate is not None
return self._animate
@property
def layout(self) -> Layout | None:
return self.styles.layout
def on_style_change(self) -> None:
self.clear_render_cache()
@@ -237,7 +251,11 @@ class Widget(DOMNode):
Returns:
RenderableType: Any renderable
"""
return Align.center(Text(f"#{self.id}"), vertical="middle")
# Default displays a pretty repr in the center of the screen
return Align.center(
Pretty(self, no_wrap=True, overflow="ellipsis"), vertical="middle"
)
async def action(self, action: str, *params) -> None:
await self.app.action(action, self)
@@ -258,11 +276,11 @@ class Widget(DOMNode):
# self.render_cache = None
self.reset_check_repaint()
self.reset_check_layout()
await self.emit(Layout(self))
await self.emit(messages.Layout(self))
elif repaint or self.check_repaint():
# self.render_cache = None
self.reset_check_repaint()
await self.emit(Update(self, self))
await self.emit(messages.Update(self, self))
async def focus(self) -> None:
await self.app.set_focus(self)