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: end-of-file-fixer
- id: check-yaml - id: check-yaml
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 21.8b0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
exclude: ^tests/ exclude: ^tests/

239
poetry.lock generated
View File

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

View File

@@ -17,16 +17,21 @@ classifiers = [
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
] ]
[tool.poetry.scripts]
textual = "textual.cli.cli:run"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = "^3.7"
rich = "^12.0.0" rich = "^12.0.0"
#rich = {git = "git@github.com:willmcgugan/rich", rev = "link-id"} #rich = {git = "git@github.com:willmcgugan/rich", rev = "link-id"}
typing-extensions = { version = "^3.10.0", python = "<3.8" } typing-extensions = { version = "^3.10.0", python = "<3.8" }
click = "8.1.2"
importlib-metadata = "^4.11.3"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^6.2.3" pytest = "^6.2.3"
black = "^22.1.0" black = "^22.3.0"
mypy = "^0.931" mypy = "^0.931"
pytest-cov = "^2.12.1" pytest-cov = "^2.12.1"
mkdocs = "^1.2.3" 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 { App > Screen {
layout: dock; layout: dock;
docks: side=left/1; docks: side=left/1;
background: $background; background: $surface;
color: $text-background; color: $text-surface;
} }
#sidebar { #sidebar {
color: $text-primary; color: $text-primary;
background: $primary; background: $primary;
@@ -66,14 +67,14 @@ App > Screen {
color: $text-background; color: $text-background;
background: $background; background: $background;
layout: vertical; layout: vertical;
overflow-y:scroll; overflow-y: scroll;
} }
Tweet { Tweet {
height: 22; height: 12;
max-width: 80; width: 80;
margin: 1 3; margin: 1 3;
background: $panel; background: $panel;
color: $text-panel; color: $text-panel;
@@ -81,7 +82,24 @@ Tweet {
/* border: outer $primary; */ /* border: outer $primary; */
padding: 1; padding: 1;
border: wide $panel-darken-2; 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 { TweetBody {
width: 100%;
background: $panel; background: $panel;
color: $text-panel; color: $text-panel;
height:20; height:20;
@@ -159,7 +178,7 @@ OptionItem:hover {
} }
Error { Error {
max-width: 80; width: 80;
height:3; height:3;
background: $error; background: $error;
color: $text-error; color: $text-error;
@@ -168,10 +187,11 @@ Error {
margin: 1 3; margin: 1 3;
text-style: bold; text-style: bold;
align-horizontal: center;
} }
Warning { Warning {
max-width: 80; width: 80;
height:3; height:3;
background: $warning; background: $warning;
color: $text-warning-fade-1; color: $text-warning-fade-1;
@@ -179,15 +199,23 @@ Warning {
border-bottom: hkey $warning-darken-2; border-bottom: hkey $warning-darken-2;
margin: 1 2; margin: 1 2;
text-style: bold; text-style: bold;
align-horizontal: center;
} }
Success { Success {
max-width: 80; width: 80;
height:3; height:3;
box-sizing: border-box;
background: $success-lighten-3; background: $success-lighten-3;
color: $text-success-lighten-3-fade-1; color: $text-success-lighten-3-fade-1;
border-top: hkey $success; border-top: hkey $success;
border-bottom: hkey $success; border-bottom: hkey $success;
margin: 1 2; margin: 1 2;
text-style: bold; text-style: bold;
align-horizontal: center;
} }
.horizontal {
layout: horizontal
}

View File

@@ -1,9 +1,47 @@
from rich.align import Align from rich.align import Align
from rich.console import RenderableType from rich.console import RenderableType
from rich.syntax import Syntax
from rich.text import Text from rich.text import Text
from textual.app import App from textual.app import App
from textual.widget import Widget 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( lorem = Text.from_markup(
@@ -56,9 +94,25 @@ class BasicApp(App):
def on_mount(self): def on_mount(self):
"""Build layout here.""" """Build layout here."""
self.mount( self.mount(
header=Widget(), header=Static(
Align.center(
"[b]This is a [u]Textual[/u] app, running in the terminal",
vertical="middle",
)
),
content=Widget( 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(), Error(),
Tweet(TweetBody()), Tweet(TweetBody()),
Warning(), Warning(),

View File

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

View File

@@ -9,7 +9,7 @@ import warnings
from asyncio import AbstractEventLoop from asyncio import AbstractEventLoop
from contextlib import redirect_stdout from contextlib import redirect_stdout
from time import perf_counter 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
import rich.repr import rich.repr
@@ -64,6 +64,7 @@ DEFAULT_COLORS = ColorSystem(
success="#6d9f71", success="#6d9f71",
accent="#ffa62b", accent="#ffa62b",
system="#5a4599", 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. 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". 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.error_console = Console(markup=False, stderr=True)
self._screen = screen self._screen = screen
self.driver_class = driver_class or self.get_driver_class() self.driver_class = driver_class or self.get_driver_class()
@@ -122,6 +125,7 @@ class App(DOMNode):
self._title = title self._title = title
self._log_console: Console | None = None self._log_console: Console | None = None
self._log_file: TextIO | None = None
if log: if log:
self._log_file = open(log, "wt") self._log_file = open(log, "wt")
self._log_console = Console( self._log_console = Console(
@@ -131,9 +135,6 @@ class App(DOMNode):
highlight=False, highlight=False,
width=100, width=100,
) )
else:
self._log_console = None
self._log_file = None
self.log_verbosity = log_verbosity self.log_verbosity = log_verbosity
@@ -459,6 +460,7 @@ class App(DOMNode):
Args: Args:
error (Exception): An exception instance. error (Exception): An exception instance.
""" """
if hasattr(error, "__rich__"): if hasattr(error, "__rich__"):
# Exception has a rich method, so we can defer to that for the rendering # Exception has a rich method, so we can defer to that for the rendering
self.panic(error) self.panic(error)
@@ -489,15 +491,9 @@ class App(DOMNode):
if os.getenv("TEXTUAL_DEVTOOLS") == "1": if os.getenv("TEXTUAL_DEVTOOLS") == "1":
try: try:
await self.devtools.connect() await self.devtools.connect()
if self._log_console: self.log(f"Connected to devtools ({self.devtools.url})")
self._log_console.print(
f"Connected to devtools ({self.devtools.url})"
)
except DevtoolsConnectionError: except DevtoolsConnectionError:
if self._log_console: self.log(f"Couldn't connect to devtools ({self.devtools.url})")
self._log_console.print(
f"Couldn't connect to devtools ({self.devtools.url})"
)
try: try:
if self.css_file is not None: if self.css_file is not None:
self.stylesheet.read(self.css_file) self.stylesheet.read(self.css_file)
@@ -532,11 +528,8 @@ class App(DOMNode):
with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore
await super().process_messages() await super().process_messages()
log("Message processing stopped") await self.animator.stop()
with timer("animator.stop()"): await self.close_all()
await self.animator.stop()
with timer("self.close_all()"):
await self.close_all()
finally: finally:
driver.stop_application_mode() driver.stop_application_mode()
except Exception as error: except Exception as error:

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 @property
def rich_color(self) -> RichColor: def rich_color(self) -> RichColor:
"""This color encoded in Rich's Color class.""" """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 r, g, b, _a = self
return RichColor.from_rgb(r, g, b) return RichColor.from_rgb(r, g, b)

View File

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

View File

@@ -14,6 +14,8 @@ from ._help_text import (
color_property_help_text, color_property_help_text,
) )
from .constants import ( from .constants import (
VALID_ALIGN_HORIZONTAL,
VALID_ALIGN_VERTICAL,
VALID_BORDER, VALID_BORDER,
VALID_BOX_SIZING, VALID_BOX_SIZING,
VALID_EDGE, VALID_EDGE,
@@ -654,3 +656,32 @@ class StylesBuilder:
transitions[css_property] = Transition(duration, easing, delay) transitions[css_property] = Transition(duration, easing, delay)
self.styles._rules["transitions"] = transitions 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_BOX_SIZING: Final = {"border-box", "content-box"}
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"} 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) NULL_SPACING: Final = Spacing.all(0)

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ from rich.console import Console
from rich.segment import Segment from rich.segment import Segment
DEFAULT_PORT = 8081 DEVTOOLS_PORT = 8081
WEBSOCKET_CONNECT_TIMEOUT = 3 WEBSOCKET_CONNECT_TIMEOUT = 3
LOG_QUEUE_MAXSIZE = 512 LOG_QUEUE_MAXSIZE = 512
@@ -90,7 +90,7 @@ class DevtoolsClient:
port (int): The port the devtools server is accessed via, defaults to 8081 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.url: str = f"ws://{host}:{port}"
self.session: aiohttp.ClientSession | None = None self.session: aiohttp.ClientSession | None = None
self.log_queue_task: Task | 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 pathlib import Path
from typing import Iterable from typing import Iterable
from importlib_metadata import version
from rich.containers import Renderables
from rich.style import Style from rich.style import Style
from rich.text import Text from rich.text import Text
@@ -21,10 +23,31 @@ from rich.rule import Rule
from rich.segment import Segment, Segments from rich.segment import Segment, Segments
from rich.table import Table 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 """Renderable representing a single log message
Args: Args:
@@ -61,18 +84,17 @@ class DevtoolsLogMessage:
file_link = escape(f"file://{Path(self.path).absolute()}") file_link = escape(f"file://{Path(self.path).absolute()}")
file_and_line = escape(f"{Path(self.path).name}:{self.line_number}") file_and_line = escape(f"{Path(self.path).name}:{self.line_number}")
table.add_row( table.add_row(
f" [#888177]{local_time.time()} [dim]{timezone_name}[/]", f"[dim]{local_time.time()} {timezone_name}",
Align.right( 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 table
yield Segments(self.segments) yield Segments(self.segments)
class DevtoolsInternalMessage: class DevConsoleNotice:
"""Renderable for messages written by the devtools server itself """Renderable for messages written by the devtools console itself
Args: Args:
message (str): The message to display message (str): The message to display
@@ -80,7 +102,7 @@ class DevtoolsInternalMessage:
Determines colors used to render the message and the perceived importance. 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.message = message
self.level = level self.level = level
@@ -89,7 +111,7 @@ class DevtoolsInternalMessage:
) -> RenderResult: ) -> RenderResult:
level_to_style = { level_to_style = {
"info": "dim", "info": "dim",
"warning": "#FFA000", "warning": "yellow",
"error": "#C52828", "error": "red",
} }
yield Rule(self.message, style=level_to_style.get(self.level, "dim")) yield Rule(self.message, style=level_to_style.get(self.level, "dim"))

View File

@@ -1,14 +1,12 @@
from __future__ import annotations from __future__ import annotations
import sys
from aiohttp.web import run_app from aiohttp.web import run_app
from aiohttp.web_app import Application from aiohttp.web_app import Application
from aiohttp.web_request import Request from aiohttp.web_request import Request
from aiohttp.web_routedef import get from aiohttp.web_routedef import get
from aiohttp.web_ws import WebSocketResponse 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 from textual.devtools.service import DevtoolsService
DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS = 2 DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS = 2
@@ -38,9 +36,13 @@ async def _on_startup(app: Application) -> None:
await service.start() await service.start()
def _run_devtools(port: int) -> None: def _run_devtools() -> None:
app = _make_devtools_aiohttp_app() 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( def _make_devtools_aiohttp_app(
@@ -65,8 +67,4 @@ def _make_devtools_aiohttp_app(
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) > 1: _run_devtools()
port = int(sys.argv[1])
else:
port = DEFAULT_PORT
_run_devtools(port)

View File

@@ -14,7 +14,11 @@ from aiohttp.web_ws import WebSocketResponse
from rich.console import Console from rich.console import Console
from rich.markup import escape 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"} QUEUEABLE_TYPES = {"client_log", "client_spillover"}
@@ -38,6 +42,7 @@ class DevtoolsService:
async def start(self): async def start(self):
"""Starts devtools tasks""" """Starts devtools tasks"""
self.size_poll_task = asyncio.create_task(self._console_size_poller()) self.size_poll_task = asyncio.create_task(self._console_size_poller())
self.console.print(DevConsoleHeader())
@property @property
def clients_connected(self) -> bool: def clients_connected(self) -> bool:
@@ -167,7 +172,7 @@ class ClientHandler:
decoded_segments = base64.b64decode(encoded_segments) decoded_segments = base64.b64decode(encoded_segments)
segments = pickle.loads(decoded_segments) segments = pickle.loads(decoded_segments)
self.service.console.print( self.service.console.print(
DevtoolsLogMessage( DevConsoleLog(
segments=segments, segments=segments,
path=path, path=path,
line_number=line_number, line_number=line_number,
@@ -176,7 +181,7 @@ class ClientHandler:
) )
elif type == "client_spillover": elif type == "client_spillover":
spillover = int(message_json["payload"]["spillover"]) spillover = int(message_json["payload"]["spillover"])
info_renderable = DevtoolsInternalMessage( info_renderable = DevConsoleNotice(
f"Discarded {spillover} messages", level="warning" f"Discarded {spillover} messages", level="warning"
) )
self.service.console.print(info_renderable) self.service.console.print(info_renderable)
@@ -198,9 +203,7 @@ class ClientHandler:
if self.request.remote: if self.request.remote:
self.service.console.print( self.service.console.print(
DevtoolsInternalMessage( DevConsoleNotice(f"Client '{escape(self.request.remote)}' connected")
f"Client '{escape(self.request.remote)}' connected"
)
) )
try: try:
await self.service.send_server_info(client_handler=self) await self.service.send_server_info(client_handler=self)
@@ -223,20 +226,16 @@ class ClientHandler:
await self.incoming_queue.put(message_json) await self.incoming_queue.put(message_json)
elif message.type == WSMsgType.ERROR: elif message.type == WSMsgType.ERROR:
self.service.console.print( self.service.console.print(
DevtoolsInternalMessage( DevConsoleNotice("Websocket error occurred", level="error")
"Websocket error occurred", level="error"
)
) )
break break
except Exception as error: except Exception as error:
self.service.console.print( self.service.console.print(DevConsoleNotice(str(error), level="error"))
DevtoolsInternalMessage(str(error), level="error")
)
finally: finally:
if self.request.remote: if self.request.remote:
self.service.console.print( self.service.console.print(
"\n", "\n",
DevtoolsInternalMessage( DevConsoleNotice(
f"Client '{escape(self.request.remote)}' disconnected" 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 __future__ import annotations
from math import sqrt
from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar
SpacingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]] SpacingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]
@@ -94,7 +93,7 @@ class Offset(NamedTuple):
""" """
x1, y1 = self x1, y1 = self
x2, y2 = other 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 return distance
@@ -665,5 +664,14 @@ class Spacing(NamedTuple):
) )
return NotImplemented 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) NULL_OFFSET = Offset(0, 0)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import sys import sys
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from operator import attrgetter
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
from .._layout_resolve import layout_resolve from .._layout_resolve import layout_resolve
@@ -91,7 +92,7 @@ class DockLayout(Layout):
add_placement = placements.append add_placement = placements.append
arranged_widgets: set[Widget] = set() 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) arranged_widgets.update(widgets)
dock_options = [make_dock_options(widget, edge) for widget in widgets] dock_options = [make_dock_options(widget, edge) for widget in widgets]

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ from typing import (
Awaitable, Awaitable,
TYPE_CHECKING, TYPE_CHECKING,
Callable, Callable,
ClassVar,
Iterable, Iterable,
NamedTuple, NamedTuple,
cast, cast,
@@ -15,22 +14,23 @@ from typing import (
import rich.repr import rich.repr
from rich.align import Align from rich.align import Align
from rich.console import Console, RenderableType from rich.console import Console, RenderableType
from rich.measure import Measurement
from rich.padding import Padding from rich.padding import Padding
from rich.pretty import Pretty
from rich.style import Style from rich.style import Style
from rich.styled import Styled from rich.styled import Styled
from rich.text import Text
from . import errors, log from . import errors, log
from . import events from . import events
from ._animator import BoundAnimator from ._animator import BoundAnimator
from ._border import Border from ._border import Border
from .box_model import BoxModel, get_box_model
from ._callback import invoke from ._callback import invoke
from .color import Color from .color import Color
from ._context import active_app from ._context import active_app
from ._types import Lines from ._types import Lines
from .dom import DOMNode from .dom import DOMNode
from .geometry import clamp, Offset, Region, Size from .geometry import clamp, Offset, Region, Size, Spacing
from .message import Message from .message import Message
from . import messages from . import messages
from .layout import Layout from .layout import Layout
@@ -96,6 +96,8 @@ class Widget(DOMNode):
super().__init__(name=name, id=id, classes=classes) super().__init__(name=name, id=id, classes=classes)
self.add_children(*children) self.add_children(*children)
auto_width = Reactive(True)
auto_height = Reactive(True)
has_focus = Reactive(False) has_focus = Reactive(False)
mouse_over = Reactive(False) mouse_over = Reactive(False)
scroll_x = Reactive(0.0, repaint=False) scroll_x = Reactive(0.0, repaint=False)
@@ -105,6 +107,34 @@ class Widget(DOMNode):
show_vertical_scrollbar = Reactive(False, layout=True) show_vertical_scrollbar = Reactive(False, layout=True)
show_horizontal_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: async def watch_scroll_x(self, new_value: float) -> None:
self.horizontal_scrollbar.position = int(new_value) self.horizontal_scrollbar.position = int(new_value)
@@ -394,10 +424,7 @@ class Widget(DOMNode):
if renderable_text_style: if renderable_text_style:
renderable = Styled(renderable, 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: if styles.border:
renderable = Border( renderable = Border(
@@ -518,7 +545,9 @@ class Widget(DOMNode):
"""Render all lines.""" """Render all lines."""
width, height = self.size width, height = self.size
renderable = self.render_styled() 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) lines = self.console.render_lines(renderable, options)
self._render_cache = RenderCache(self.size, lines) self._render_cache = RenderCache(self.size, lines)
self._dirty_regions.clear() self._dirty_regions.clear()
@@ -656,13 +685,13 @@ class Widget(DOMNode):
def on_mouse_scroll_down(self, event) -> None: def on_mouse_scroll_down(self, event) -> None:
if self.is_container: if self.is_container:
if not self.scroll_down(animate=False): self.scroll_down(animate=False)
event.stop() event.stop()
def on_mouse_scroll_up(self, event) -> None: def on_mouse_scroll_up(self, event) -> None:
if self.is_container: if self.is_container:
if not self.scroll_up(animate=False): self.scroll_up(animate=False)
event.stop() event.stop()
def handle_scroll_to(self, message: ScrollTo) -> None: def handle_scroll_to(self, message: ScrollTo) -> None:
if self.is_container: if self.is_container:

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 rich.segment import Segment
from tests.utilities.render import wait_for_predicate from tests.utilities.render import wait_for_predicate
from textual.devtools.renderables import DevtoolsLogMessage, DevtoolsInternalMessage from textual.devtools.renderables import DevConsoleLog, DevConsoleNotice
TIMESTAMP = 1649166819 TIMESTAMP = 1649166819
WIDTH = 40 WIDTH = 40
@@ -31,7 +31,7 @@ def console():
@time_machine.travel(TIMESTAMP) @time_machine.travel(TIMESTAMP)
def test_log_message_render(console): def test_log_message_render(console):
message = DevtoolsLogMessage( message = DevConsoleLog(
[Segment("content")], [Segment("content")],
path="abc/hello.py", path="abc/hello.py",
line_number=123, line_number=123,
@@ -56,13 +56,13 @@ def test_log_message_render(console):
timezone_name = local_time.tzname() timezone_name = local_time.tzname()
string_timestamp = local_time.time() 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 right.align == "right"
assert "hello.py:123" in right.renderable assert "hello.py:123" in right.renderable
def test_internal_message_render(console): def test_internal_message_render(console):
message = DevtoolsInternalMessage("hello") message = DevConsoleNotice("hello")
rule = next(iter(message.__rich_console__(console, console.options))) rule = next(iter(message.__rich_console__(console, console.options)))
assert rule.title == "hello" assert rule.title == "hello"
assert rule.characters == "" 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" 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(): def test_split():
assert Region(10, 5, 22, 15).split(10, 5) == ( assert Region(10, 5, 22, 15).split(10, 5) == (
Region(10, 5, 10, 5), Region(10, 5, 10, 5),