Merge branch 'css' of github.com:willmcgugan/textual into style-error-improvements

This commit is contained in:
Darren Burns
2022-04-22 16:25:19 +01:00
34 changed files with 935 additions and 317 deletions

View File

@@ -8,7 +8,7 @@ repos:
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/psf/black
rev: 21.8b0
rev: 22.3.0
hooks:
- id: black
exclude: ^tests/

239
poetry.lock generated
View File

@@ -85,7 +85,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
[[package]]
name = "black"
version = "22.1.0"
version = "22.3.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
@@ -96,7 +96,7 @@ click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = ">=1.1.0"
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\""}
@@ -135,11 +135,11 @@ unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "8.0.4"
version = "8.1.2"
description = "Composable command line interface toolkit"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
@@ -149,7 +149,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@@ -240,7 +240,7 @@ python-versions = ">=3.5"
name = "importlib-metadata"
version = "4.11.3"
description = "Read metadata from Python packages"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.7"
@@ -263,11 +263,11 @@ python-versions = "*"
[[package]]
name = "jinja2"
version = "3.0.3"
version = "3.1.1"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.0"
@@ -307,7 +307,7 @@ python-versions = ">=3.6"
[[package]]
name = "mkdocs"
version = "1.2.3"
version = "1.3.0"
description = "Project documentation with Markdown."
category = "dev"
optional = false
@@ -316,8 +316,8 @@ python-versions = ">=3.6"
[package.dependencies]
click = ">=3.3"
ghp-import = ">=1.0"
importlib-metadata = ">=3.10"
Jinja2 = ">=2.10.1"
importlib-metadata = ">=4.3"
Jinja2 = ">=2.10.2"
Markdown = ">=3.2.1"
mergedeep = ">=1.3.4"
packaging = ">=20.5"
@@ -444,15 +444,15 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "platformdirs"
version = "2.5.1"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
[[package]]
name = "pluggy"
@@ -471,11 +471,11 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "2.17.0"
version = "2.18.1"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.6.1"
python-versions = ">=3.7"
[package.dependencies]
cfgv = ">=2.0.0"
@@ -515,14 +515,14 @@ Markdown = ">=3.2"
[[package]]
name = "pyparsing"
version = "3.0.7"
description = "Python parsing module"
version = "3.0.8"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
diagrams = ["railroad-diagrams", "jinja2"]
[[package]]
name = "pytest"
@@ -641,7 +641,7 @@ pyyaml = "*"
[[package]]
name = "rich"
version = "12.0.1"
version = "12.1.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false
@@ -650,7 +650,7 @@ python-versions = ">=3.6.2,<4.0.0"
[package.dependencies]
commonmark = ">=0.9.0,<0.10.0"
pygments = ">=2.6.0,<3.0.0"
typing-extensions = {version = ">=3.7.4,<5.0", markers = "python_version < \"3.8\""}
typing-extensions = {version = ">=3.7.4,<5.0", markers = "python_version < \"3.9\""}
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
@@ -692,7 +692,7 @@ python-versions = ">=3.7"
[[package]]
name = "typed-ast"
version = "1.5.2"
version = "1.5.3"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
@@ -708,7 +708,7 @@ python-versions = "*"
[[package]]
name = "virtualenv"
version = "20.13.4"
version = "20.14.1"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
@@ -727,7 +727,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)",
[[package]]
name = "watchdog"
version = "2.1.6"
version = "2.1.7"
description = "Filesystem events monitoring"
category = "dev"
optional = false
@@ -751,20 +751,20 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[[package]]
name = "zipp"
version = "3.7.0"
version = "3.8.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "24cdf0a574b7ae21c2ea5c407075b044b9cd01f7d2d01dd562e3cf33d958d88c"
content-hash = "256c1d6571a11bf4b80d0eba16d9e39bf2965c4436281c3ec40033cca54aa098"
[metadata.files]
aiohttp = [
@@ -866,29 +866,29 @@ attrs = [
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
black = [
{file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"},
{file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"},
{file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"},
{file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"},
{file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"},
{file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"},
{file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"},
{file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"},
{file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"},
{file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"},
{file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"},
{file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"},
{file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"},
{file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"},
{file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"},
{file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"},
{file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"},
{file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"},
{file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"},
{file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"},
{file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"},
{file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"},
{file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"},
{file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"},
{file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"},
{file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"},
{file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"},
{file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"},
{file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"},
{file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"},
{file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"},
{file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"},
{file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"},
{file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"},
{file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"},
{file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"},
{file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"},
{file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"},
{file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"},
{file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"},
{file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"},
{file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"},
{file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"},
{file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"},
{file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"},
{file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"},
]
cached-property = [
{file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
@@ -903,8 +903,8 @@ charset-normalizer = [
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
]
click = [
{file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
{file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
{file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"},
{file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@@ -1047,8 +1047,8 @@ iniconfig = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
jinja2 = [
{file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
{file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
{file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"},
{file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"},
]
markdown = [
{file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"},
@@ -1101,8 +1101,8 @@ mergedeep = [
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
mkdocs = [
{file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"},
{file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"},
{file = "mkdocs-1.3.0-py3-none-any.whl", hash = "sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde"},
{file = "mkdocs-1.3.0.tar.gz", hash = "sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea"},
]
mkdocs-autorefs = [
{file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
@@ -1220,16 +1220,16 @@ pathspec = [
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
platformdirs = [
{file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"},
{file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"},
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
pre-commit = [
{file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"},
{file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"},
{file = "pre_commit-2.18.1-py2.py3-none-any.whl", hash = "sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2"},
{file = "pre_commit-2.18.1.tar.gz", hash = "sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
@@ -1244,8 +1244,8 @@ pymdown-extensions = [
{file = "pymdown_extensions-9.3-py3-none-any.whl", hash = "sha256:b37461a181c1c8103cfe1660081726a0361a8294cbfda88e5b02cefe976f0546"},
]
pyparsing = [
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
{file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"},
{file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"},
]
pytest = [
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
@@ -1312,8 +1312,8 @@ pyyaml-env-tag = [
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
rich = [
{file = "rich-12.0.1-py3-none-any.whl", hash = "sha256:ce5c714e984a2d185399e4e1dd1f8b2feacb7cecfc576f1522425643a36a57ea"},
{file = "rich-12.0.1.tar.gz", hash = "sha256:3fba9dd15ebe048e2795a02ac19baee79dc12cc50b074ef70f2958cd651b59a9"},
{file = "rich-12.1.0-py3-none-any.whl", hash = "sha256:b60ff99f4ff7e3d1d37444dee2b22fdd941c622dbc37841823ec1ce7f058b263"},
{file = "rich-12.1.0.tar.gz", hash = "sha256:198ae15807a7c1bf84ceabf662e902731bf8f874f9e775e2289cab02bb6a4e30"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -1370,30 +1370,30 @@ tomli = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
typed-ast = [
{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"},
{file = "typed_ast-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ad3b48cf2b487be140072fb86feff36801487d4abb7382bb1929aaac80638ea"},
{file = "typed_ast-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:542cd732351ba8235f20faa0fc7398946fe1a57f2cdb289e5497e1e7f48cfedb"},
{file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc2c11ae59003d4a26dda637222d9ae924387f96acae9492df663843aefad55"},
{file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd5df1313915dbd70eaaa88c19030b441742e8b05e6103c631c83b75e0435ccc"},
{file = "typed_ast-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:e34f9b9e61333ecb0f7d79c21c28aa5cd63bec15cb7e1310d7d3da6ce886bc9b"},
{file = "typed_ast-1.5.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f818c5b81966d4728fec14caa338e30a70dfc3da577984d38f97816c4b3071ec"},
{file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3042bfc9ca118712c9809201f55355479cfcdc17449f9f8db5e744e9625c6805"},
{file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fff9fdcce59dc61ec1b317bdb319f8f4e6b69ebbe61193ae0a60c5f9333dc49"},
{file = "typed_ast-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8e0b8528838ffd426fea8d18bde4c73bcb4167218998cc8b9ee0a0f2bfe678a6"},
{file = "typed_ast-1.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef1d96ad05a291f5c36895d86d1375c0ee70595b90f6bb5f5fdbee749b146db"},
{file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed44e81517364cb5ba367e4f68fca01fba42a7a4690d40c07886586ac267d9b9"},
{file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f60d9de0d087454c91b3999a296d0c4558c1666771e3460621875021bf899af9"},
{file = "typed_ast-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9e237e74fd321a55c90eee9bc5d44be976979ad38a29bbd734148295c1ce7617"},
{file = "typed_ast-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee852185964744987609b40aee1d2eb81502ae63ee8eef614558f96a56c1902d"},
{file = "typed_ast-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27e46cdd01d6c3a0dd8f728b6a938a6751f7bd324817501c15fb056307f918c6"},
{file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d64dabc6336ddc10373922a146fa2256043b3b43e61f28961caec2a5207c56d5"},
{file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8cdf91b0c466a6c43f36c1964772918a2c04cfa83df8001ff32a89e357f8eb06"},
{file = "typed_ast-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:9cc9e1457e1feb06b075c8ef8aeb046a28ec351b1958b42c7c31c989c841403a"},
{file = "typed_ast-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e20d196815eeffb3d76b75223e8ffed124e65ee62097e4e73afb5fec6b993e7a"},
{file = "typed_ast-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37e5349d1d5de2f4763d534ccb26809d1c24b180a477659a12c4bde9dd677d74"},
{file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f1a27592fac87daa4e3f16538713d705599b0a27dfe25518b80b6b017f0a6d"},
{file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8831479695eadc8b5ffed06fdfb3e424adc37962a75925668deeb503f446c0a3"},
{file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"},
{file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
@@ -1401,33 +1401,34 @@ typing-extensions = [
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
virtualenv = [
{file = "virtualenv-20.13.4-py2.py3-none-any.whl", hash = "sha256:c3e01300fb8495bc00ed70741f5271fc95fed067eb7106297be73d30879af60c"},
{file = "virtualenv-20.13.4.tar.gz", hash = "sha256:ce8901d3bbf3b90393498187f2d56797a8a452fb2d0d7efc6fd837554d6f679c"},
{file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"},
{file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"},
]
watchdog = [
{file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"},
{file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"},
{file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"},
{file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"},
{file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"},
{file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"},
{file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"},
{file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"},
{file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"},
{file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"},
{file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"},
{file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"},
{file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"},
{file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"},
{file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"},
{file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"},
{file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"},
{file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"},
{file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"},
{file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"},
{file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"},
{file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"},
{file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"},
{file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383"},
{file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4"},
{file = "watchdog-2.1.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca"},
{file = "watchdog-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4d0e98ac2e8dd803a56f4e10438b33a2d40390a72750cff4939b4b274e7906fa"},
{file = "watchdog-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:81982c7884aac75017a6ecc72f1a4fedbae04181a8665a34afce9539fc1b3fab"},
{file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0b4a1fe6201c6e5a1926f5767b8664b45f0fcb429b62564a41f490ff1ce1dc7a"},
{file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6e6ae29b72977f2e1ee3d0b760d7ee47896cb53e831cbeede3e64485e5633cc8"},
{file = "watchdog-2.1.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b9777664848160449e5b4260e0b7bc1ae0f6f4992a8b285db4ec1ef119ffa0e2"},
{file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:19b36d436578eb437e029c6b838e732ed08054956366f6dd11875434a62d2b99"},
{file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b61acffaf5cd5d664af555c0850f9747cc5f2baf71e54bbac164c58398d6ca7b"},
{file = "watchdog-2.1.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e877c70245424b06c41ac258023ea4bd0c8e4ff15d7c1368f17cd0ae6e351dd"},
{file = "watchdog-2.1.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d802d65262a560278cf1a65ef7cae4e2bc7ecfe19e5451349e4c67e23c9dc420"},
{file = "watchdog-2.1.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b3750ee5399e6e9c69eae8b125092b871ee9e2fcbd657a92747aea28f9056a5c"},
{file = "watchdog-2.1.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:ed6d9aad09a2a948572224663ab00f8975fae242aa540509737bb4507133fa2d"},
{file = "watchdog-2.1.7-py3-none-manylinux2014_armv7l.whl", hash = "sha256:b26e13e8008dcaea6a909e91d39b629a39635d1a8a7239dd35327c74f4388601"},
{file = "watchdog-2.1.7-py3-none-manylinux2014_i686.whl", hash = "sha256:0908bb50f6f7de54d5d31ec3da1654cb7287c6b87bce371954561e6de379d690"},
{file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64.whl", hash = "sha256:bdcbf75580bf4b960fb659bbccd00123d83119619195f42d721e002c1621602f"},
{file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:81a5861d0158a7e55fe149335fb2bbfa6f48cbcbd149b52dbe2cd9a544034bbd"},
{file = "watchdog-2.1.7-py3-none-manylinux2014_s390x.whl", hash = "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385"},
{file = "watchdog-2.1.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055"},
{file = "watchdog-2.1.7-py3-none-win32.whl", hash = "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6"},
{file = "watchdog-2.1.7-py3-none-win_amd64.whl", hash = "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566"},
{file = "watchdog-2.1.7-py3-none-win_ia64.whl", hash = "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572"},
{file = "watchdog-2.1.7.tar.gz", hash = "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480"},
]
yarl = [
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
@@ -1504,6 +1505,6 @@ yarl = [
{file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
]
zipp = [
{file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"},
{file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"},
{file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
{file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
]

View File

@@ -17,16 +17,21 @@ classifiers = [
"Programming Language :: Python :: 3.10",
]
[tool.poetry.scripts]
textual = "textual.cli.cli:run"
[tool.poetry.dependencies]
python = "^3.7"
rich = "^12.0.0"
#rich = {git = "git@github.com:willmcgugan/rich", rev = "link-id"}
typing-extensions = { version = "^3.10.0", python = "<3.8" }
click = "8.1.2"
importlib-metadata = "^4.11.3"
[tool.poetry.dev-dependencies]
pytest = "^6.2.3"
black = "^22.1.0"
black = "^22.3.0"
mypy = "^0.931"
pytest-cov = "^2.12.1"
mkdocs = "^1.2.3"

46
sandbox/align.css Normal file
View File

@@ -0,0 +1,46 @@
Screen {
layout: vertical;
overflow: auto;
}
Widget {
margin:1;
}
#thing {
width: auto;
height: 10;
background:magenta;
margin: 3;
padding: 1;
border: solid white;
box-sizing: border-box;
border: solid white;
align-horizontal: center;
}
#thing2 {
border: solid white;
/* outline: heavy blue; */
height: 10;
padding: 1 2;
box-sizing: border-box;
max-height: 100vh;
background:green;
align-horizontal: center;
color:white;
}
#thing3 {
height: 10;
margin: 1;
background:blue;
align-horizontal: center;
}

25
sandbox/align.py Normal file
View File

@@ -0,0 +1,25 @@
from rich.text import Text
from textual.app import App
from textual.widget import Widget
from textual.widgets import Static
class Thing(Widget):
def render(self):
return Text.from_markup("Hello, World. [b magenta]Lorem impsum.")
class AlignApp(App):
def on_load(self):
self.bind("t", "log_tree")
def on_mount(self) -> None:
self.log("MOUNTED")
self.mount(thing=Thing(), thing2=Static("0123456789"), thing3=Widget())
def action_log_tree(self):
self.log(self.screen.tree)
AlignApp.run(css_file="align.css", log="textual.log", watch_css=True)

View File

@@ -17,10 +17,11 @@
App > Screen {
layout: dock;
docks: side=left/1;
background: $background;
color: $text-background;
background: $surface;
color: $text-surface;
}
#sidebar {
color: $text-primary;
background: $primary;
@@ -66,14 +67,14 @@ App > Screen {
color: $text-background;
background: $background;
layout: vertical;
overflow-y:scroll;
overflow-y: scroll;
}
Tweet {
height: 22;
max-width: 80;
height: 12;
width: 80;
margin: 1 3;
background: $panel;
color: $text-panel;
@@ -81,7 +82,24 @@ Tweet {
/* border: outer $primary; */
padding: 1;
border: wide $panel-darken-2;
overflow-y: scroll
overflow-y: scroll;
align-horizontal: center;
}
.scrollable {
width: 80;
overflow-y: scroll;
max-width:80;
height: 20;
align-horizontal: center;
layout: vertical;
}
.code {
height: 34;
width: 100%;
}
@@ -92,6 +110,7 @@ TweetHeader {
}
TweetBody {
width: 100%;
background: $panel;
color: $text-panel;
height:20;
@@ -159,7 +178,7 @@ OptionItem:hover {
}
Error {
max-width: 80;
width: 80;
height:3;
background: $error;
color: $text-error;
@@ -168,10 +187,11 @@ Error {
margin: 1 3;
text-style: bold;
align-horizontal: center;
}
Warning {
max-width: 80;
width: 80;
height:3;
background: $warning;
color: $text-warning-fade-1;
@@ -179,15 +199,23 @@ Warning {
border-bottom: hkey $warning-darken-2;
margin: 1 2;
text-style: bold;
align-horizontal: center;
}
Success {
max-width: 80;
width: 80;
height:3;
box-sizing: border-box;
background: $success-lighten-3;
color: $text-success-lighten-3-fade-1;
border-top: hkey $success;
border-bottom: hkey $success;
margin: 1 2;
text-style: bold;
align-horizontal: center;
}
.horizontal {
layout: horizontal
}

View File

@@ -1,9 +1,47 @@
from rich.align import Align
from rich.console import RenderableType
from rich.syntax import Syntax
from rich.text import Text
from textual.app import App
from textual.widget import Widget
from textual.widgets import Static
CODE = '''
class Offset(NamedTuple):
"""A point defined by x and y coordinates."""
x: int = 0
y: int = 0
@property
def is_origin(self) -> bool:
"""Check if the point is at the origin (0, 0)"""
return self == (0, 0)
def __bool__(self) -> bool:
return self != (0, 0)
def __add__(self, other: object) -> Offset:
if isinstance(other, tuple):
_x, _y = self
x, y = other
return Offset(_x + x, _y + y)
return NotImplemented
def __sub__(self, other: object) -> Offset:
if isinstance(other, tuple):
_x, _y = self
x, y = other
return Offset(_x - x, _y - y)
return NotImplemented
def __mul__(self, other: object) -> Offset:
if isinstance(other, (float, int)):
x, y = self
return Offset(int(x * other), int(y * other))
return NotImplemented
'''
lorem = Text.from_markup(
@@ -56,9 +94,25 @@ class BasicApp(App):
def on_mount(self):
"""Build layout here."""
self.mount(
header=Widget(),
header=Static(
Align.center(
"[b]This is a [u]Textual[/u] app, running in the terminal",
vertical="middle",
)
),
content=Widget(
Tweet(TweetBody(), Widget(classes={"button"})),
Tweet(
TweetBody(),
# Widget(
# Widget(classes={"button"}),
# Widget(classes={"button"}),
# classes={"horizontal"},
# ),
),
Widget(
Static(Syntax(CODE, "python"), classes={"code"}),
classes={"scrollable"},
),
Error(),
Tweet(TweetBody()),
Warning(),

View File

@@ -8,6 +8,7 @@
.list-item {
height: 8;
min-width: 80;
background: dark_blu;
padding: 2x;

View File

@@ -9,7 +9,7 @@ import warnings
from asyncio import AbstractEventLoop
from contextlib import redirect_stdout
from time import perf_counter
from typing import Any, Iterable, Type, TYPE_CHECKING
from typing import Any, Iterable, TextIO, Type, TYPE_CHECKING
import rich
import rich.repr
@@ -64,6 +64,7 @@ DEFAULT_COLORS = ColorSystem(
success="#6d9f71",
accent="#ffa62b",
system="#5a4599",
dark_surface="#292929",
)
@@ -100,7 +101,9 @@ class App(DOMNode):
driver_class (Type[Driver], optional): Driver class, or None to use default. Defaults to None.
title (str, optional): Title of the application. Defaults to "Textual Application".
"""
self.console = Console(markup=False, highlight=False, emoji=False)
self.console = Console(
file=sys.__stdout__, markup=False, highlight=False, emoji=False
)
self.error_console = Console(markup=False, stderr=True)
self._screen = screen
self.driver_class = driver_class or self.get_driver_class()
@@ -122,6 +125,7 @@ class App(DOMNode):
self._title = title
self._log_console: Console | None = None
self._log_file: TextIO | None = None
if log:
self._log_file = open(log, "wt")
self._log_console = Console(
@@ -131,9 +135,6 @@ class App(DOMNode):
highlight=False,
width=100,
)
else:
self._log_console = None
self._log_file = None
self.log_verbosity = log_verbosity
@@ -459,6 +460,7 @@ class App(DOMNode):
Args:
error (Exception): An exception instance.
"""
if hasattr(error, "__rich__"):
# Exception has a rich method, so we can defer to that for the rendering
self.panic(error)
@@ -489,15 +491,9 @@ class App(DOMNode):
if os.getenv("TEXTUAL_DEVTOOLS") == "1":
try:
await self.devtools.connect()
if self._log_console:
self._log_console.print(
f"Connected to devtools ({self.devtools.url})"
)
self.log(f"Connected to devtools ({self.devtools.url})")
except DevtoolsConnectionError:
if self._log_console:
self._log_console.print(
f"Couldn't connect to devtools ({self.devtools.url})"
)
self.log(f"Couldn't connect to devtools ({self.devtools.url})")
try:
if self.css_file is not None:
self.stylesheet.read(self.css_file)
@@ -532,10 +528,7 @@ class App(DOMNode):
with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore
await super().process_messages()
log("Message processing stopped")
with timer("animator.stop()"):
await self.animator.stop()
with timer("self.close_all()"):
await self.close_all()
finally:
driver.stop_application_mode()

88
src/textual/box_model.py Normal file
View File

@@ -0,0 +1,88 @@
from __future__ import annotations
from operator import is_
from typing import Callable, NamedTuple, TYPE_CHECKING
from .geometry import Size, Spacing
from .css.styles import StylesBase
class BoxModel(NamedTuple):
"""The result of `get_box_model`."""
size: Size # Content + padding + border
margin: Spacing # Additional margin
def get_box_model(
styles: StylesBase,
container: Size,
viewport: Size,
get_content_width: Callable[[Size, Size], int],
get_content_height: Callable[[Size, Size], int],
) -> BoxModel:
"""Resolve the box model for this Styles.
Args:
styles (StylesBase): Styles object.
container (Size): The size of the widget container.
viewport (Size): The viewport size.
get_auto_width (Callable): A callable which accepts container size and parent size and returns a width.
get_auto_height (Callable): A callable which accepts container size and parent size and returns a height.
Returns:
BoxModel: A tuple with the size of the content area and margin.
"""
has_rule = styles.has_rule
width, height = container
is_content_box = styles.box_sizing == "content-box"
gutter = styles.padding + styles.border.spacing
if not has_rule("width"):
width = container.width
elif styles.width.is_auto:
# When width is auto, we want enough space to always fit the content
width = get_content_width(container, viewport)
if not is_content_box:
# If box sizing is border box we want to enlarge the width so that it
# can accommodate padding + border
width += gutter.width
else:
width = styles.width.resolve_dimension(container, viewport)
if not has_rule("height"):
height = container.height
elif styles.height.is_auto:
height = get_content_height(container, viewport)
if not is_content_box:
height += gutter.height
else:
height = styles.height.resolve_dimension(container, viewport)
if is_content_box:
gutter_width, gutter_height = gutter.totals
width += gutter_width
height += gutter_height
if has_rule("min_width"):
min_width = styles.min_width.resolve_dimension(container, viewport)
width = max(width, min_width)
if has_rule("max_width"):
max_width = styles.max_width.resolve_dimension(container, viewport)
width = min(width, max_width)
if has_rule("min_height"):
min_height = styles.min_height.resolve_dimension(container, viewport)
height = max(height, min_height)
if has_rule("max_height"):
max_height = styles.max_height.resolve_dimension(container, viewport)
height = min(height, max_height)
size = Size(width, height)
margin = styles.margin
return BoxModel(size, margin)

View File

15
src/textual/cli/cli.py Normal file
View File

@@ -0,0 +1,15 @@
import click
from importlib_metadata import version
from textual.devtools.server import _run_devtools
@click.group()
@click.version_option(version("textual"))
def run():
pass
@run.command(help="Run the Textual Devtools console")
def console():
_run_devtools()

View File

@@ -139,6 +139,7 @@ class Color(NamedTuple):
@property
def rich_color(self) -> RichColor:
"""This color encoded in Rich's Color class."""
# TODO: This isn't cheap as I'd like - cache in a LRUCache ?
r, g, b, _a = self
return RichColor.from_rgb(r, g, b)

View File

@@ -14,6 +14,8 @@ from typing import Iterable, NamedTuple, TYPE_CHECKING, cast
import rich.repr
from rich.style import Style
from ._help_text import scalar_help_text
from .. import log
from ._help_text import (
spacing_wrong_number_of_values,
scalar_help_text,
@@ -50,10 +52,14 @@ class ScalarProperty:
"""Descriptor for getting and setting scalar properties. Scalars are numeric values with a unit, e.g. "50vh"."""
def __init__(
self, units: set[Unit] | None = None, percent_unit: Unit = Unit.WIDTH
self,
units: set[Unit] | None = None,
percent_unit: Unit = Unit.WIDTH,
allow_auto: bool = True,
) -> None:
self.units: set[Unit] = units or {*UNIT_SYMBOL}
self.percent_unit = percent_unit
self.allow_auto = allow_auto
super().__init__()
def __set_name__(self, owner: Styles, name: str) -> None:
@@ -96,7 +102,7 @@ class ScalarProperty:
obj.clear_rule(self.name)
obj.refresh(layout=True)
return
if isinstance(value, float) or isinstance(value, int):
if isinstance(value, (int, float)):
new_value = Scalar(float(value), Unit.CELLS, Unit.WIDTH)
elif isinstance(value, Scalar):
new_value = value
@@ -112,12 +118,23 @@ class ScalarProperty:
)
else:
raise StyleValueError("expected float, int, Scalar, or None")
if (
new_value is not None
and new_value.unit == Unit.AUTO
and not self.allow_auto
):
raise StyleValueError("'auto' not allowed here")
if new_value.unit != Unit.AUTO:
if new_value is not None and new_value.unit not in self.units:
raise StyleValueError(
f"{self.name} units must be one of {friendly_list(get_symbols(self.units))}"
)
if new_value is not None and new_value.is_percent:
new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH)
new_value = Scalar(
float(new_value.value), self.percent_unit, Unit.WIDTH
)
if obj.set_rule(self.name, new_value):
obj.refresh(layout=True)

View File

@@ -14,6 +14,8 @@ from ._help_text import (
color_property_help_text,
)
from .constants import (
VALID_ALIGN_HORIZONTAL,
VALID_ALIGN_VERTICAL,
VALID_BORDER,
VALID_BOX_SIZING,
VALID_EDGE,
@@ -654,3 +656,32 @@ class StylesBuilder:
transitions[css_property] = Transition(duration, easing, delay)
self.styles._rules["transitions"] = transitions
def process_align(self, name: str, tokens: list[Token]) -> None:
if len(tokens) != 2:
self.error(name, tokens[0], "expected two tokens")
token_horizontal = tokens[0]
token_vertical = tokens[1]
if token_horizontal.name != "token":
self.error(
name,
token_horizontal,
f"invalid token {token_horizontal!r}, expected {friendly_list(VALID_ALIGN_HORIZONTAL)}",
)
if token_vertical.name != "token":
self.error(
name,
token_vertical,
f"invalid token {token_vertical!r}, expected {friendly_list(VALID_ALIGN_VERTICAL)}",
)
self.styles._rules["align_horizontal"] = token_horizontal.value
self.styles._rules["align_vertical"] = token_vertical.value
def process_align_horizontal(self, name: str, tokens: list[Token]) -> None:
value = self._process_enum(name, tokens, VALID_ALIGN_HORIZONTAL)
self.styles._rules["align_horizontal"] = value
def process_align_vertical(self, name: str, tokens: list[Token]) -> None:
value = self._process_enum(name, tokens, VALID_ALIGN_VERTICAL)
self.styles._rules["align_vertical"] = value

View File

@@ -29,6 +29,7 @@ VALID_LAYOUT: Final = {"dock", "vertical", "grid"}
VALID_BOX_SIZING: Final = {"border-box", "content-box"}
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"}
VALID_ALIGN_HORIZONTAL: Final = {"left", "center", "right"}
VALID_ALIGN_VERTICAL: Final = {"top", "middle", "bottom"}
NULL_SPACING: Final = Spacing.all(0)

View File

@@ -3,11 +3,10 @@ from __future__ import annotations
from enum import Enum, unique
from functools import lru_cache
import re
from typing import Iterable, NamedTuple, TYPE_CHECKING
from typing import Callable, Iterable, NamedTuple, TYPE_CHECKING
import rich.repr
from textual.css.tokenizer import Token
from .. import log
from ..geometry import Offset
@@ -34,6 +33,7 @@ class Unit(Enum):
HEIGHT = 5
VIEW_WIDTH = 6
VIEW_HEIGHT = 7
AUTO = 8
UNIT_SYMBOL = {
@@ -107,6 +107,10 @@ class Scalar(NamedTuple):
def symbol(self) -> str:
return UNIT_SYMBOL[self.unit]
@property
def is_auto(self) -> bool:
return self.unit == Unit.AUTO
@classmethod
def from_number(cls, value: float) -> Scalar:
return cls(float(value), Unit.CELLS, Unit.WIDTH)
@@ -124,6 +128,9 @@ class Scalar(NamedTuple):
Returns:
Scalar: New scalar
"""
if token.lower() == "auto":
scalar = cls(1.0, Unit.AUTO, Unit.AUTO)
else:
match = _MATCH_SCALAR(token)
if match is None:
raise ScalarParseError(f"{token!r} is not a valid scalar")
@@ -142,12 +149,13 @@ class Scalar(NamedTuple):
viewport (tuple[int, int]): Size of the viewport (typically terminal size)
Raises:
ScalarResolveError: _description_
ScalarResolveError: If the unit is unknown.
Returns:
float: _description_
int: A size (in cells)
"""
value, unit, percent_unit = self
if unit == Unit.PERCENT:
unit = percent_unit
try:

View File

@@ -13,7 +13,7 @@ from rich.style import Style
from .. import log
from .._animator import Animation, EasingFunction
from ..color import Color
from ..geometry import Size, Spacing
from ..geometry import Offset, Size, Spacing
from ._style_properties import (
BorderProperty,
BoxProperty,
@@ -32,17 +32,28 @@ from ._style_properties import (
TransitionsProperty,
FractionalProperty,
)
from .constants import VALID_BOX_SIZING, VALID_DISPLAY, VALID_VISIBILITY, VALID_OVERFLOW
from .constants import (
VALID_ALIGN_HORIZONTAL,
VALID_ALIGN_VERTICAL,
VALID_BOX_SIZING,
VALID_DISPLAY,
VALID_VISIBILITY,
VALID_OVERFLOW,
)
from .scalar import Scalar, ScalarOffset, Unit
from .scalar_animation import ScalarAnimation
from .transition import Transition
from .types import (
BoxSizing,
Display,
AlignHorizontal,
AlignVertical,
Edge,
AlignHorizontal,
Overflow,
Specificity3,
Specificity4,
AlignVertical,
Visibility,
)
@@ -115,8 +126,12 @@ class RulesMap(TypedDict, total=False):
scrollbar_background_hover: Color
scrollbar_background_active: Color
align_horizontal: AlignHorizontal
align_vertical: AlignVertical
RULE_NAMES = list(RulesMap.__annotations__.keys())
RULE_NAMES_SET = frozenset(RULE_NAMES)
_rule_getter = attrgetter(*RULE_NAMES)
@@ -178,10 +193,10 @@ class StylesBase(ABC):
box_sizing = StringEnumProperty(VALID_BOX_SIZING, "border-box", layout=True)
width = ScalarProperty(percent_unit=Unit.WIDTH)
height = ScalarProperty(percent_unit=Unit.HEIGHT)
min_width = ScalarProperty(percent_unit=Unit.WIDTH)
min_height = ScalarProperty(percent_unit=Unit.HEIGHT)
max_width = ScalarProperty(percent_unit=Unit.WIDTH)
max_height = ScalarProperty(percent_unit=Unit.HEIGHT)
min_width = ScalarProperty(percent_unit=Unit.WIDTH, allow_auto=False)
min_height = ScalarProperty(percent_unit=Unit.HEIGHT, allow_auto=False)
max_width = ScalarProperty(percent_unit=Unit.WIDTH, allow_auto=False)
max_height = ScalarProperty(percent_unit=Unit.HEIGHT, allow_auto=False)
dock = DockProperty()
docks = DocksProperty()
@@ -203,6 +218,9 @@ class StylesBase(ABC):
scrollbar_background_hover = ColorProperty("#444444")
scrollbar_background_active = ColorProperty("black")
align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
def __eq__(self, styles: object) -> bool:
"""Check that Styles containts the same rules."""
if not isinstance(styles, StylesBase):
@@ -344,70 +362,43 @@ class StylesBase(ABC):
else:
return None
def get_box_model(
self, container_size: Size, parent_size: Size
) -> tuple[Size, Spacing]:
"""Resolve the box model for this Styles.
def align_width(self, width: int, parent_width: int) -> int:
"""Align the width dimension.
Args:
container_size (Size): The size of the widget container.
parent_size (Size): The size widget's parent.
width (int): Width of the content.
parent_width (int): Width of the parent container.
Returns:
tuple[Size, Spacing]: A tuple with the size of the content area and margin.
int: An offset to add to the X coordinate.
"""
has_rule = self.has_rule
width, height = container_size
if has_rule("width"):
width = self.width.resolve_dimension(container_size, parent_size)
offset_x = 0
align_horizontal = self.align_horizontal
if align_horizontal != "left":
if align_horizontal == "center":
offset_x = (parent_width - width) // 2
else:
width = max(0, width - self.margin.width)
offset_x = parent_width - width
return offset_x
if self.min_width:
min_width = self.min_width.resolve_dimension(container_size, parent_size)
width = max(width, min_width)
def align_height(self, height: int, parent_height: int) -> int:
"""Align the height dimensions
if self.max_width:
max_width = self.max_width.resolve_dimension(container_size, parent_size)
width = min(width, max_width)
Args:
height (int): Height of the content.
parent_height (int): Height of the parent container.
if has_rule("height"):
height = self.height.resolve_dimension(container_size, parent_size)
Returns:
int: An offset to add to the Y coordinate.
"""
offset_y = 0
align_vertical = self.align_vertical
if align_vertical != "top":
if align_vertical == "middle":
offset_y = (parent_height - height) // 2
else:
height = max(0, height - self.margin.height)
if self.min_height:
min_height = self.min_height.resolve_dimension(container_size, parent_size)
height = max(height, min_height)
if self.max_height:
max_height = self.max_height.resolve_dimension(container_size, parent_size)
height = min(width, max_height)
# TODO: box sizing
size = Size(width, height)
margin = Spacing(0, 0, 0, 0)
if self.box_sizing == "content-box":
if has_rule("padding"):
size += self.padding.totals
if has_rule("border"):
size += self.border.spacing.totals
if has_rule("margin"):
margin = self.margin
else: # border-box
if has_rule("padding"):
size -= self.padding.totals
if has_rule("border"):
size -= self.border.spacing.totals
if has_rule("margin"):
margin = self.margin
return size, margin
offset_y = parent_height - height
return offset_y
@rich.repr.auto
@@ -425,6 +416,7 @@ class Styles(StylesBase):
return Styles(node=self.node, _rules=self.get_rules(), important=self.important)
def has_rule(self, rule: str) -> bool:
assert rule in RULE_NAMES_SET, f"no such rule {rule!r}"
return rule in self._rules
def clear_rule(self, rule: str) -> bool:
@@ -675,6 +667,15 @@ class Styles(StylesBase):
),
)
if has_rule("align_horizontal") and has_rule("align_vertical"):
append_declaration(
"align", f"{self.align_horizontal} {self.align_vertical}"
)
elif has_rule("align_horizontal"):
append_declaration("align-horizontal", self.align_horizontal)
elif has_rule("align_horizontal"):
append_declaration("align-vertical", self.align_vertical)
lines.sort()
return lines

View File

@@ -30,6 +30,8 @@ EdgeType = Literal[
]
Visibility = Literal["visible", "hidden", "initial", "inherit"]
Display = Literal["block", "none"]
AlignHorizontal = Literal["left", "center", "right"]
AlignVertical = Literal["top", "middle", "bottom"]
BoxSizing = Literal["border-box", "content-box"]
Overflow = Literal["scroll", "hidden", "auto"]
EdgeStyle = Tuple[str, Color]

View File

@@ -16,7 +16,7 @@ from rich.console import Console
from rich.segment import Segment
DEFAULT_PORT = 8081
DEVTOOLS_PORT = 8081
WEBSOCKET_CONNECT_TIMEOUT = 3
LOG_QUEUE_MAXSIZE = 512
@@ -90,7 +90,7 @@ class DevtoolsClient:
port (int): The port the devtools server is accessed via, defaults to 8081
"""
def __init__(self, host: str = "127.0.0.1", port: int = DEFAULT_PORT) -> None:
def __init__(self, host: str = "127.0.0.1", port: int = DEVTOOLS_PORT) -> None:
self.url: str = f"ws://{host}:{port}"
self.session: aiohttp.ClientSession | None = None
self.log_queue_task: Task | None = None

View File

@@ -5,6 +5,8 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable
from importlib_metadata import version
from rich.containers import Renderables
from rich.style import Style
from rich.text import Text
@@ -21,10 +23,31 @@ from rich.rule import Rule
from rich.segment import Segment, Segments
from rich.table import Table
DevtoolsMessageLevel = Literal["info", "warning", "error"]
DevConsoleMessageLevel = Literal["info", "warning", "error"]
class DevtoolsLogMessage:
class DevConsoleHeader:
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
lines = Renderables(
[
f"[bold]Textual Development Console [magenta]v{version('textual')}",
"[magenta]Run a Textual app with the environment variable [b]TEXTUAL_DEVTOOLS=1[/] to connect.",
"[magenta]Press [b]Ctrl+C[/] to quit.",
]
)
render_options = options.update(width=options.max_width - 4)
lines = console.render_lines(lines, render_options)
new_line = Segment("\n")
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:
@@ -61,18 +84,17 @@ class DevtoolsLogMessage:
file_link = escape(f"file://{Path(self.path).absolute()}")
file_and_line = escape(f"{Path(self.path).name}:{self.line_number}")
table.add_row(
f" [#888177]{local_time.time()} [dim]{timezone_name}[/]",
f"[dim]{local_time.time()} {timezone_name}",
Align.right(
Text(f"{file_and_line} ", style=Style(color="#888177", link=file_link))
Text(f"{file_and_line}", style=Style(dim=True, link=file_link))
),
style="on #292724",
)
yield table
yield Segments(self.segments)
class DevtoolsInternalMessage:
"""Renderable for messages written by the devtools server itself
class DevConsoleNotice:
"""Renderable for messages written by the devtools console itself
Args:
message (str): The message to display
@@ -80,7 +102,7 @@ class DevtoolsInternalMessage:
Determines colors used to render the message and the perceived importance.
"""
def __init__(self, message: str, *, level: DevtoolsMessageLevel = "info") -> None:
def __init__(self, message: str, *, level: DevConsoleMessageLevel = "info") -> None:
self.message = message
self.level = level
@@ -89,7 +111,7 @@ class DevtoolsInternalMessage:
) -> RenderResult:
level_to_style = {
"info": "dim",
"warning": "#FFA000",
"error": "#C52828",
"warning": "yellow",
"error": "red",
}
yield Rule(self.message, style=level_to_style.get(self.level, "dim"))

View File

@@ -1,14 +1,12 @@
from __future__ import annotations
import sys
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 textual.devtools.client import DEFAULT_PORT
from textual.devtools.client import DEVTOOLS_PORT
from textual.devtools.service import DevtoolsService
DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS = 2
@@ -38,9 +36,13 @@ async def _on_startup(app: Application) -> None:
await service.start()
def _run_devtools(port: int) -> None:
def _run_devtools() -> None:
app = _make_devtools_aiohttp_app()
run_app(app, port=port)
def noop_print(_: str):
return None
run_app(app, port=DEVTOOLS_PORT, print=noop_print)
def _make_devtools_aiohttp_app(
@@ -65,8 +67,4 @@ def _make_devtools_aiohttp_app(
if __name__ == "__main__":
if len(sys.argv) > 1:
port = int(sys.argv[1])
else:
port = DEFAULT_PORT
_run_devtools(port)
_run_devtools()

View File

@@ -14,7 +14,11 @@ from aiohttp.web_ws import WebSocketResponse
from rich.console import Console
from rich.markup import escape
from textual.devtools.renderables import DevtoolsLogMessage, DevtoolsInternalMessage
from textual.devtools.renderables import (
DevConsoleLog,
DevConsoleNotice,
DevConsoleHeader,
)
QUEUEABLE_TYPES = {"client_log", "client_spillover"}
@@ -38,6 +42,7 @@ class DevtoolsService:
async def start(self):
"""Starts devtools tasks"""
self.size_poll_task = asyncio.create_task(self._console_size_poller())
self.console.print(DevConsoleHeader())
@property
def clients_connected(self) -> bool:
@@ -167,7 +172,7 @@ class ClientHandler:
decoded_segments = base64.b64decode(encoded_segments)
segments = pickle.loads(decoded_segments)
self.service.console.print(
DevtoolsLogMessage(
DevConsoleLog(
segments=segments,
path=path,
line_number=line_number,
@@ -176,7 +181,7 @@ class ClientHandler:
)
elif type == "client_spillover":
spillover = int(message_json["payload"]["spillover"])
info_renderable = DevtoolsInternalMessage(
info_renderable = DevConsoleNotice(
f"Discarded {spillover} messages", level="warning"
)
self.service.console.print(info_renderable)
@@ -198,9 +203,7 @@ class ClientHandler:
if self.request.remote:
self.service.console.print(
DevtoolsInternalMessage(
f"Client '{escape(self.request.remote)}' connected"
)
DevConsoleNotice(f"Client '{escape(self.request.remote)}' connected")
)
try:
await self.service.send_server_info(client_handler=self)
@@ -223,20 +226,16 @@ class ClientHandler:
await self.incoming_queue.put(message_json)
elif message.type == WSMsgType.ERROR:
self.service.console.print(
DevtoolsInternalMessage(
"Websocket error occurred", level="error"
)
DevConsoleNotice("Websocket error occurred", level="error")
)
break
except Exception as error:
self.service.console.print(
DevtoolsInternalMessage(str(error), level="error")
)
self.service.console.print(DevConsoleNotice(str(error), level="error"))
finally:
if self.request.remote:
self.service.console.print(
"\n",
DevtoolsInternalMessage(
DevConsoleNotice(
f"Client '{escape(self.request.remote)}' disconnected"
),
)

View File

@@ -6,7 +6,6 @@ Functions and classes to manage terminal geometry (anything involving coordinate
from __future__ import annotations
from math import sqrt
from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar
SpacingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]
@@ -94,7 +93,7 @@ class Offset(NamedTuple):
"""
x1, y1 = self
x2, y2 = other
distance = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
distance = ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) ** 0.5
return distance
@@ -665,5 +664,14 @@ class Spacing(NamedTuple):
)
return NotImplemented
def __sub__(self, other: object) -> Spacing:
if isinstance(other, tuple):
top1, right1, bottom1, left1 = self
top2, right2, bottom2, left2 = other
return Spacing(
top1 - top2, right1 - right2, bottom1 - bottom2, left1 - left2
)
return NotImplemented
NULL_OFFSET = Offset(0, 0)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import sys
from collections import defaultdict
from dataclasses import dataclass
from operator import attrgetter
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
from .._layout_resolve import layout_resolve
@@ -91,7 +92,7 @@ class DockLayout(Layout):
add_placement = placements.append
arranged_widgets: set[Widget] = set()
for edge, widgets, z in docks:
for z, (edge, widgets, _z) in enumerate(sorted(docks, key=attrgetter("z"))):
arranged_widgets.update(widgets)
dock_options = [make_dock_options(widget, edge) for widget in widgets]

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from typing import cast
from textual.geometry import Size, Offset, Region
from textual.layout import Layout, WidgetPlacement
@@ -24,15 +25,30 @@ class HorizontalLayout(Layout):
x = max_width = max_height = 0
parent_size = parent.size
for widget in parent.children:
(content_width, content_height), margin = widget.styles.get_box_model(
size, parent_size
)
region = Region(margin.left + x, margin.top, content_width, content_height)
max_height = max(max_height, content_height + margin.height)
box_models = [
widget.get_box_model(size, parent_size)
for widget in cast("list[Widget]", parent.children)
]
margins = [
max((box1.margin.right, box2.margin.left))
for box1, box2 in zip(box_models, box_models[1:])
]
if box_models:
margins.append(box_models[-1].margin.right)
x = box_models[0].margin.left if box_models else 0
for widget, box_model, margin in zip(parent.children, box_models, margins):
content_width, content_height = box_model.size
offset_y = widget.styles.align_height(content_height, parent_size.height)
region = Region(x, offset_y, content_width, content_height)
max_height = max(max_height, content_height)
add_placement(WidgetPlacement(region, widget, 0))
x += region.width + margin.left
max_width = x + margin.right
x += region.width + margin
max_width = x
max_width += margins[-1] if margins else 0
total_region = Region(0, 0, max_width, max_height)
add_placement(WidgetPlacement(total_region, None, 0))

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import cast, TYPE_CHECKING
from .. import log
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
class VerticalLayout(Layout):
"""Simple vertical layout."""
"""Used to layout Widgets vertically on screen, from top to bottom."""
name = "vertical"
@@ -26,15 +26,30 @@ class VerticalLayout(Layout):
y = max_width = max_height = 0
parent_size = parent.size
for widget in parent.children:
(content_width, content_height), margin = widget.styles.get_box_model(
size, parent_size
)
region = Region(margin.left, y + margin.top, content_width, content_height)
max_width = max(max_width, content_width + margin.width)
box_models = [
widget.get_box_model(size, parent_size)
for widget in cast("list[Widget]", parent.children)
]
margins = [
max((box1.margin.bottom, box2.margin.top))
for box1, box2 in zip(box_models, box_models[1:])
]
if box_models:
margins.append(box_models[-1].margin.bottom)
y = box_models[0].margin.top if box_models else 0
for widget, box_model, margin in zip(parent.children, box_models, margins):
content_width, content_height = box_model.size
offset_x = widget.styles.align_width(content_width, parent_size.width)
region = Region(offset_x, y, content_width, content_height)
max_height = max(max_height, content_height)
add_placement(WidgetPlacement(region, widget, 0))
y += region.height + margin.top
max_height = y + margin.bottom
y += region.height + margin
max_height = y
max_height += margins[-1] if margins else 0
total_region = Region(0, 0, max_width, max_height)
add_placement(WidgetPlacement(total_region, None, 0))

View File

@@ -1,19 +1,19 @@
from __future__ import annotations
from rich.console import ConsoleOptions, Console, RenderResult
from rich.color import Color
from rich.segment import Segment
from rich.style import Style
from ._blend_colors import blend_colors_rgb
from ..color import Color
class VerticalGradient:
"""Draw a vertical gradient."""
def __init__(self, color1: str, color2: str) -> None:
self._color1 = Color.parse(color1).get_truecolor()
self._color2 = Color.parse(color2).get_truecolor()
self._color1 = Color.parse(color1)
self._color2 = Color.parse(color2)
def __rich_console__(
self, console: Console, options: ConsoleOptions
@@ -22,15 +22,20 @@ class VerticalGradient:
height = options.height or options.max_height
color1 = self._color1
color2 = self._color2
default_color = Color.default()
default_color = Color(0, 0, 0).rich_color
from_color = Style.from_color
blend = color1.blend
rich_color1 = color1.rich_color
for y in range(height):
yield Segment(
f"{width * ' '}\n",
from_color(
default_color, blend_colors_rgb(color1, color2, y / (height - 1))
line_color = from_color(
default_color,
(
blend(color2, y / (height - 1)).rich_color
if height > 1
else rich_color1
),
)
yield Segment(f"{width * ' '}\n", line_color)
if __name__ == "__main__":

View File

@@ -131,7 +131,8 @@ class Screen(Widget):
)
)
except Exception as error:
self.app.panic(error)
self.app.on_exception(error)
return
self.app.refresh()
self._dirty_widgets.clear()
@@ -204,6 +205,10 @@ class Screen(Widget):
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)
if widget is self:
event.set_forwarded()
await self.post_message(event)
else:
await widget.forward_event(event.offset(-region.x, -region.y))
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
@@ -213,6 +218,9 @@ class Screen(Widget):
return
scroll_widget = widget
if scroll_widget is not None:
if scroll_widget is self:
await self.post_message(event)
else:
await scroll_widget.forward_event(event)
else:
await self.post_message(event)

View File

@@ -6,7 +6,6 @@ from typing import (
Awaitable,
TYPE_CHECKING,
Callable,
ClassVar,
Iterable,
NamedTuple,
cast,
@@ -15,22 +14,23 @@ from typing import (
import rich.repr
from rich.align import Align
from rich.console import Console, RenderableType
from rich.measure import Measurement
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
from . import errors, log
from . import events
from ._animator import BoundAnimator
from ._border import Border
from .box_model import BoxModel, get_box_model
from ._callback import invoke
from .color import Color
from ._context import active_app
from ._types import Lines
from .dom import DOMNode
from .geometry import clamp, Offset, Region, Size
from .geometry import clamp, Offset, Region, Size, Spacing
from .message import Message
from . import messages
from .layout import Layout
@@ -96,6 +96,8 @@ class Widget(DOMNode):
super().__init__(name=name, id=id, classes=classes)
self.add_children(*children)
auto_width = Reactive(True)
auto_height = Reactive(True)
has_focus = Reactive(False)
mouse_over = Reactive(False)
scroll_x = Reactive(0.0, repaint=False)
@@ -105,6 +107,34 @@ class Widget(DOMNode):
show_vertical_scrollbar = Reactive(False, layout=True)
show_horizontal_scrollbar = Reactive(False, layout=True)
def get_box_model(self, container: Size, viewport: Size) -> BoxModel:
"""Process the box model for this widget.
Args:
container (Size): The size of the container widget (with a layout)
viewport (Size): The viewport size.
Returns:
BoxModel: The size and margin for this widget.
"""
box_model = get_box_model(
self.styles,
container,
viewport,
self.get_content_width,
self.get_content_height,
)
return box_model
def get_content_width(self, container_size: Size, parent_size: Size) -> int:
console = self.app.console
renderable = self.render()
measurement = Measurement.get(console, console.options, renderable)
return measurement.maximum
def get_content_height(self, container_size: Size, parent_size: Size) -> int:
return container_size.height
async def watch_scroll_x(self, new_value: float) -> None:
self.horizontal_scrollbar.position = int(new_value)
@@ -394,10 +424,7 @@ class Widget(DOMNode):
if renderable_text_style:
renderable = Styled(renderable, renderable_text_style)
if styles.padding:
renderable = Padding(
renderable, styles.padding, style=renderable_text_style
)
renderable = Padding(renderable, styles.padding, style=renderable_text_style)
if styles.border:
renderable = Border(
@@ -518,7 +545,9 @@ class Widget(DOMNode):
"""Render all lines."""
width, height = self.size
renderable = self.render_styled()
options = self.console.options.update_dimensions(width, height)
options = self.console.options.update_dimensions(width, height).update(
highlight=False
)
lines = self.console.render_lines(renderable, options)
self._render_cache = RenderCache(self.size, lines)
self._dirty_regions.clear()
@@ -656,12 +685,12 @@ class Widget(DOMNode):
def on_mouse_scroll_down(self, event) -> None:
if self.is_container:
if not self.scroll_down(animate=False):
self.scroll_down(animate=False)
event.stop()
def on_mouse_scroll_up(self, event) -> None:
if self.is_container:
if not self.scroll_up(animate=False):
self.scroll_up(animate=False)
event.stop()
def handle_scroll_to(self, message: ScrollTo) -> None:

10
tests/cli/test_cli.py Normal file
View File

@@ -0,0 +1,10 @@
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

View File

@@ -7,7 +7,7 @@ from rich.console import Console
from rich.segment import Segment
from tests.utilities.render import wait_for_predicate
from textual.devtools.renderables import DevtoolsLogMessage, DevtoolsInternalMessage
from textual.devtools.renderables import DevConsoleLog, DevConsoleNotice
TIMESTAMP = 1649166819
WIDTH = 40
@@ -31,7 +31,7 @@ def console():
@time_machine.travel(TIMESTAMP)
def test_log_message_render(console):
message = DevtoolsLogMessage(
message = DevConsoleLog(
[Segment("content")],
path="abc/hello.py",
line_number=123,
@@ -56,13 +56,13 @@ def test_log_message_render(console):
timezone_name = local_time.tzname()
string_timestamp = local_time.time()
assert left == f" [#888177]{string_timestamp} [dim]{timezone_name}[/]"
assert left == f"[dim]{string_timestamp} {timezone_name}"
assert right.align == "right"
assert "hello.py:123" in right.renderable
def test_internal_message_render(console):
message = DevtoolsInternalMessage("hello")
message = DevConsoleNotice("hello")
rule = next(iter(message.__rich_console__(console, console.options)))
assert rule.title == "hello"
assert rule.characters == ""

183
tests/test_box_model.py Normal file
View File

@@ -0,0 +1,183 @@
from __future__ import annotations
from textual.box_model import BoxModel, get_box_model
from textual.css.styles import Styles
from textual.geometry import Size, Spacing
def test_content_box():
styles = Styles()
styles.width = 10
styles.height = 8
styles.padding = 1
styles.border = ("solid", "red")
# border-box is default
assert styles.box_sizing == "border-box"
def get_auto_width(container: Size, parent: Size) -> int:
assert False, "must not be called"
def get_auto_height(container: Size, parent: Size) -> int:
assert False, "must not be called"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
# Size should be inclusive of padding / border
assert box_model == BoxModel(Size(10, 8), Spacing(0, 0, 0, 0))
# Switch to content-box
styles.box_sizing = "content-box"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
# width and height have added padding / border to accommodate content
assert box_model == BoxModel(Size(14, 12), Spacing(0, 0, 0, 0))
def test_width():
"""Test width settings."""
styles = Styles()
def get_auto_width(container: Size, parent: Size) -> int:
return 10
def get_auto_height(container: Size, parent: Size) -> int:
return 10
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(60, 20), Spacing(0, 0, 0, 0))
# Add a margin and check that it is reported
styles.margin = (1, 2, 3, 4)
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(60, 20), Spacing(1, 2, 3, 4))
# Set width to auto-detect
styles.width = "auto"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
# Setting width to auto should call get_auto_width
assert box_model == BoxModel(Size(10, 20), Spacing(1, 2, 3, 4))
# Set width to 100 vw which should make it the width of the parent
styles.width = "100vw"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(80, 20), Spacing(1, 2, 3, 4))
# Set the width to 100% should make it fill the container size
styles.width = "100%"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(60, 20), Spacing(1, 2, 3, 4))
styles.width = "100vw"
styles.max_width = "50%"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(30, 20), Spacing(1, 2, 3, 4))
def test_height():
"""Test width settings."""
styles = Styles()
def get_auto_width(container: Size, parent: Size) -> int:
return 10
def get_auto_height(container: Size, parent: Size) -> int:
return 10
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(60, 20), Spacing(0, 0, 0, 0))
# Add a margin and check that it is reported
styles.margin = (1, 2, 3, 4)
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(60, 20), Spacing(1, 2, 3, 4))
# Set width to 100 vw which should make it the width of the parent
styles.height = "100vh"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(60, 24), Spacing(1, 2, 3, 4))
# Set the width to 100% should make it fill the container size
styles.height = "100%"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(60, 20), Spacing(1, 2, 3, 4))
styles.height = "100vh"
styles.max_height = "50%"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(60, 10), Spacing(1, 2, 3, 4))
def test_max():
"""Check that max_width and max_height are respected."""
styles = Styles()
styles.width = 100
styles.height = 80
styles.max_width = 40
styles.max_height = 30
def get_auto_width(container: Size, parent: Size) -> int:
assert False, "must not be called"
def get_auto_height(container: Size, parent: Size) -> int:
assert False, "must not be called"
box_model = get_box_model(
styles, Size(40, 30), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(40, 30), Spacing(0, 0, 0, 0))
def test_min():
"""Check that min_width and min_height are respected."""
styles = Styles()
styles.width = 10
styles.height = 5
styles.min_width = 40
styles.min_height = 30
def get_auto_width(container: Size, parent: Size) -> int:
assert False, "must not be called"
def get_auto_height(container: Size, parent: Size) -> int:
assert False, "must not be called"
box_model = get_box_model(
styles, Size(40, 30), Size(80, 24), get_auto_width, get_auto_height
)
assert box_model == BoxModel(Size(40, 30), Spacing(0, 0, 0, 0))

View File

@@ -317,6 +317,13 @@ def test_spacing_add():
Spacing(1, 2, 3, 4) + "foo"
def test_spacing_sub():
assert Spacing(1, 2, 3, 4) - Spacing(5, 6, 7, 8) == Spacing(-4, -4, -4, -4)
with pytest.raises(TypeError):
Spacing(1, 2, 3, 4) - "foo"
def test_split():
assert Region(10, 5, 22, 15).split(10, 5) == (
Region(10, 5, 10, 5),