diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e26fb347e..e4369ce2e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: end-of-file-fixer - id: check-yaml - repo: https://github.com/psf/black - rev: 21.8b0 + rev: 22.3.0 hooks: - id: black exclude: ^tests/ diff --git a/poetry.lock b/poetry.lock index 46dcb6541..3182e7961 100644 --- a/poetry.lock +++ b/poetry.lock @@ -85,7 +85,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "black" -version = "22.1.0" +version = "22.3.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -96,7 +96,7 @@ click = ">=8.0.0" mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = ">=1.1.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} @@ -135,11 +135,11 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.4" +version = "8.1.2" description = "Composable command line interface toolkit" -category = "dev" +category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -149,7 +149,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -240,7 +240,7 @@ python-versions = ">=3.5" name = "importlib-metadata" version = "4.11.3" description = "Read metadata from Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -263,11 +263,11 @@ python-versions = "*" [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.1" description = "A very fast and expressive template engine." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=2.0" @@ -307,7 +307,7 @@ python-versions = ">=3.6" [[package]] name = "mkdocs" -version = "1.2.3" +version = "1.3.0" description = "Project documentation with Markdown." category = "dev" optional = false @@ -316,8 +316,8 @@ python-versions = ">=3.6" [package.dependencies] click = ">=3.3" ghp-import = ">=1.0" -importlib-metadata = ">=3.10" -Jinja2 = ">=2.10.1" +importlib-metadata = ">=4.3" +Jinja2 = ">=2.10.2" Markdown = ">=3.2.1" mergedeep = ">=1.3.4" packaging = ">=20.5" @@ -444,15 +444,15 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "platformdirs" -version = "2.5.1" +version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] [[package]] name = "pluggy" @@ -471,11 +471,11 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.17.0" +version = "2.18.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] cfgv = ">=2.0.0" @@ -515,14 +515,14 @@ Markdown = ">=3.2" [[package]] name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" +version = "3.0.8" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "pytest" @@ -641,7 +641,7 @@ pyyaml = "*" [[package]] name = "rich" -version = "12.0.1" +version = "12.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false @@ -650,7 +650,7 @@ python-versions = ">=3.6.2,<4.0.0" [package.dependencies] commonmark = ">=0.9.0,<0.10.0" pygments = ">=2.6.0,<3.0.0" -typing-extensions = {version = ">=3.7.4,<5.0", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.4,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] @@ -692,7 +692,7 @@ python-versions = ">=3.7" [[package]] name = "typed-ast" -version = "1.5.2" +version = "1.5.3" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -708,7 +708,7 @@ python-versions = "*" [[package]] name = "virtualenv" -version = "20.13.4" +version = "20.14.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -727,7 +727,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [[package]] name = "watchdog" -version = "2.1.6" +version = "2.1.7" description = "Filesystem events monitoring" category = "dev" optional = false @@ -751,20 +751,20 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.7.0" +version = "3.8.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "24cdf0a574b7ae21c2ea5c407075b044b9cd01f7d2d01dd562e3cf33d958d88c" +content-hash = "256c1d6571a11bf4b80d0eba16d9e39bf2965c4436281c3ec40033cca54aa098" [metadata.files] aiohttp = [ @@ -866,29 +866,29 @@ attrs = [ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] black = [ - {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, - {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, - {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, - {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, - {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, - {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, - {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, - {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, - {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, - {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, - {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, - {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, - {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, - {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, - {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, - {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, - {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, - {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, - {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, ] cached-property = [ {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, @@ -903,8 +903,8 @@ charset-normalizer = [ {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, + {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"}, + {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1047,8 +1047,8 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, + {file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"}, + {file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"}, ] markdown = [ {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, @@ -1101,8 +1101,8 @@ mergedeep = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] mkdocs = [ - {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, - {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, + {file = "mkdocs-1.3.0-py3-none-any.whl", hash = "sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde"}, + {file = "mkdocs-1.3.0.tar.gz", hash = "sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea"}, ] mkdocs-autorefs = [ {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, @@ -1220,16 +1220,16 @@ pathspec = [ {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] platformdirs = [ - {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, - {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, - {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, + {file = "pre_commit-2.18.1-py2.py3-none-any.whl", hash = "sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2"}, + {file = "pre_commit-2.18.1.tar.gz", hash = "sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -1244,8 +1244,8 @@ pymdown-extensions = [ {file = "pymdown_extensions-9.3-py3-none-any.whl", hash = "sha256:b37461a181c1c8103cfe1660081726a0361a8294cbfda88e5b02cefe976f0546"}, ] pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, + {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, + {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -1312,8 +1312,8 @@ pyyaml-env-tag = [ {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] rich = [ - {file = "rich-12.0.1-py3-none-any.whl", hash = "sha256:ce5c714e984a2d185399e4e1dd1f8b2feacb7cecfc576f1522425643a36a57ea"}, - {file = "rich-12.0.1.tar.gz", hash = "sha256:3fba9dd15ebe048e2795a02ac19baee79dc12cc50b074ef70f2958cd651b59a9"}, + {file = "rich-12.1.0-py3-none-any.whl", hash = "sha256:b60ff99f4ff7e3d1d37444dee2b22fdd941c622dbc37841823ec1ce7f058b263"}, + {file = "rich-12.1.0.tar.gz", hash = "sha256:198ae15807a7c1bf84ceabf662e902731bf8f874f9e775e2289cab02bb6a4e30"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1370,30 +1370,30 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typed-ast = [ - {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, - {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, - {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, - {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, - {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, - {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, - {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"}, - {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"}, - {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"}, - {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"}, + {file = "typed_ast-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ad3b48cf2b487be140072fb86feff36801487d4abb7382bb1929aaac80638ea"}, + {file = "typed_ast-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:542cd732351ba8235f20faa0fc7398946fe1a57f2cdb289e5497e1e7f48cfedb"}, + {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc2c11ae59003d4a26dda637222d9ae924387f96acae9492df663843aefad55"}, + {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd5df1313915dbd70eaaa88c19030b441742e8b05e6103c631c83b75e0435ccc"}, + {file = "typed_ast-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:e34f9b9e61333ecb0f7d79c21c28aa5cd63bec15cb7e1310d7d3da6ce886bc9b"}, + {file = "typed_ast-1.5.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f818c5b81966d4728fec14caa338e30a70dfc3da577984d38f97816c4b3071ec"}, + {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3042bfc9ca118712c9809201f55355479cfcdc17449f9f8db5e744e9625c6805"}, + {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fff9fdcce59dc61ec1b317bdb319f8f4e6b69ebbe61193ae0a60c5f9333dc49"}, + {file = "typed_ast-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8e0b8528838ffd426fea8d18bde4c73bcb4167218998cc8b9ee0a0f2bfe678a6"}, + {file = "typed_ast-1.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef1d96ad05a291f5c36895d86d1375c0ee70595b90f6bb5f5fdbee749b146db"}, + {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed44e81517364cb5ba367e4f68fca01fba42a7a4690d40c07886586ac267d9b9"}, + {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f60d9de0d087454c91b3999a296d0c4558c1666771e3460621875021bf899af9"}, + {file = "typed_ast-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9e237e74fd321a55c90eee9bc5d44be976979ad38a29bbd734148295c1ce7617"}, + {file = "typed_ast-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee852185964744987609b40aee1d2eb81502ae63ee8eef614558f96a56c1902d"}, + {file = "typed_ast-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27e46cdd01d6c3a0dd8f728b6a938a6751f7bd324817501c15fb056307f918c6"}, + {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d64dabc6336ddc10373922a146fa2256043b3b43e61f28961caec2a5207c56d5"}, + {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8cdf91b0c466a6c43f36c1964772918a2c04cfa83df8001ff32a89e357f8eb06"}, + {file = "typed_ast-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:9cc9e1457e1feb06b075c8ef8aeb046a28ec351b1958b42c7c31c989c841403a"}, + {file = "typed_ast-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e20d196815eeffb3d76b75223e8ffed124e65ee62097e4e73afb5fec6b993e7a"}, + {file = "typed_ast-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37e5349d1d5de2f4763d534ccb26809d1c24b180a477659a12c4bde9dd677d74"}, + {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f1a27592fac87daa4e3f16538713d705599b0a27dfe25518b80b6b017f0a6d"}, + {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8831479695eadc8b5ffed06fdfb3e424adc37962a75925668deeb503f446c0a3"}, + {file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"}, + {file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"}, ] typing-extensions = [ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, @@ -1401,33 +1401,34 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] virtualenv = [ - {file = "virtualenv-20.13.4-py2.py3-none-any.whl", hash = "sha256:c3e01300fb8495bc00ed70741f5271fc95fed067eb7106297be73d30879af60c"}, - {file = "virtualenv-20.13.4.tar.gz", hash = "sha256:ce8901d3bbf3b90393498187f2d56797a8a452fb2d0d7efc6fd837554d6f679c"}, + {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, + {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, ] watchdog = [ - {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, - {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, - {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"}, - {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"}, - {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"}, - {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"}, - {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"}, - {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"}, - {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"}, - {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, + {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383"}, + {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4"}, + {file = "watchdog-2.1.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca"}, + {file = "watchdog-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4d0e98ac2e8dd803a56f4e10438b33a2d40390a72750cff4939b4b274e7906fa"}, + {file = "watchdog-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:81982c7884aac75017a6ecc72f1a4fedbae04181a8665a34afce9539fc1b3fab"}, + {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0b4a1fe6201c6e5a1926f5767b8664b45f0fcb429b62564a41f490ff1ce1dc7a"}, + {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6e6ae29b72977f2e1ee3d0b760d7ee47896cb53e831cbeede3e64485e5633cc8"}, + {file = "watchdog-2.1.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b9777664848160449e5b4260e0b7bc1ae0f6f4992a8b285db4ec1ef119ffa0e2"}, + {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:19b36d436578eb437e029c6b838e732ed08054956366f6dd11875434a62d2b99"}, + {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b61acffaf5cd5d664af555c0850f9747cc5f2baf71e54bbac164c58398d6ca7b"}, + {file = "watchdog-2.1.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e877c70245424b06c41ac258023ea4bd0c8e4ff15d7c1368f17cd0ae6e351dd"}, + {file = "watchdog-2.1.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d802d65262a560278cf1a65ef7cae4e2bc7ecfe19e5451349e4c67e23c9dc420"}, + {file = "watchdog-2.1.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b3750ee5399e6e9c69eae8b125092b871ee9e2fcbd657a92747aea28f9056a5c"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:ed6d9aad09a2a948572224663ab00f8975fae242aa540509737bb4507133fa2d"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_armv7l.whl", hash = "sha256:b26e13e8008dcaea6a909e91d39b629a39635d1a8a7239dd35327c74f4388601"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_i686.whl", hash = "sha256:0908bb50f6f7de54d5d31ec3da1654cb7287c6b87bce371954561e6de379d690"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64.whl", hash = "sha256:bdcbf75580bf4b960fb659bbccd00123d83119619195f42d721e002c1621602f"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:81a5861d0158a7e55fe149335fb2bbfa6f48cbcbd149b52dbe2cd9a544034bbd"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_s390x.whl", hash = "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055"}, + {file = "watchdog-2.1.7-py3-none-win32.whl", hash = "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6"}, + {file = "watchdog-2.1.7-py3-none-win_amd64.whl", hash = "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566"}, + {file = "watchdog-2.1.7-py3-none-win_ia64.whl", hash = "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572"}, + {file = "watchdog-2.1.7.tar.gz", hash = "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480"}, ] yarl = [ {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, @@ -1504,6 +1505,6 @@ yarl = [ {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, ] zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, ] diff --git a/pyproject.toml b/pyproject.toml index 6af02a1d5..3b110a12d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,16 +17,21 @@ classifiers = [ "Programming Language :: Python :: 3.10", ] +[tool.poetry.scripts] +textual = "textual.cli.cli:run" + [tool.poetry.dependencies] python = "^3.7" rich = "^12.0.0" #rich = {git = "git@github.com:willmcgugan/rich", rev = "link-id"} typing-extensions = { version = "^3.10.0", python = "<3.8" } +click = "8.1.2" +importlib-metadata = "^4.11.3" [tool.poetry.dev-dependencies] pytest = "^6.2.3" -black = "^22.1.0" +black = "^22.3.0" mypy = "^0.931" pytest-cov = "^2.12.1" mkdocs = "^1.2.3" diff --git a/sandbox/align.css b/sandbox/align.css new file mode 100644 index 000000000..f976925ed --- /dev/null +++ b/sandbox/align.css @@ -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; +} diff --git a/sandbox/align.py b/sandbox/align.py new file mode 100644 index 000000000..59984b57e --- /dev/null +++ b/sandbox/align.py @@ -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) diff --git a/sandbox/basic.css b/sandbox/basic.css index 4d3e64d65..5d2446655 100644 --- a/sandbox/basic.css +++ b/sandbox/basic.css @@ -17,10 +17,11 @@ App > Screen { layout: dock; docks: side=left/1; - background: $background; - color: $text-background; + background: $surface; + color: $text-surface; } + #sidebar { color: $text-primary; background: $primary; @@ -66,14 +67,14 @@ App > Screen { color: $text-background; background: $background; layout: vertical; - overflow-y:scroll; - + overflow-y: scroll; } Tweet { - height: 22; - max-width: 80; + height: 12; + width: 80; + margin: 1 3; background: $panel; color: $text-panel; @@ -81,7 +82,24 @@ Tweet { /* border: outer $primary; */ padding: 1; border: wide $panel-darken-2; - overflow-y: scroll + overflow-y: scroll; + align-horizontal: center; +} + +.scrollable { + width: 80; + overflow-y: scroll; + max-width:80; + height: 20; + align-horizontal: center; + layout: vertical; +} + +.code { + + height: 34; + width: 100%; + } @@ -92,6 +110,7 @@ TweetHeader { } TweetBody { + width: 100%; background: $panel; color: $text-panel; height:20; @@ -159,7 +178,7 @@ OptionItem:hover { } Error { - max-width: 80; + width: 80; height:3; background: $error; color: $text-error; @@ -168,10 +187,11 @@ Error { margin: 1 3; text-style: bold; + align-horizontal: center; } Warning { - max-width: 80; + width: 80; height:3; background: $warning; color: $text-warning-fade-1; @@ -179,15 +199,23 @@ Warning { border-bottom: hkey $warning-darken-2; margin: 1 2; text-style: bold; + align-horizontal: center; } Success { - max-width: 80; - height:3; + width: 80; + height:3; + box-sizing: border-box; background: $success-lighten-3; color: $text-success-lighten-3-fade-1; border-top: hkey $success; border-bottom: hkey $success; margin: 1 2; text-style: bold; + align-horizontal: center; } + + +.horizontal { + layout: horizontal +} \ No newline at end of file diff --git a/sandbox/basic.py b/sandbox/basic.py index bddf8431a..e89f54034 100644 --- a/sandbox/basic.py +++ b/sandbox/basic.py @@ -1,9 +1,47 @@ from rich.align import Align from rich.console import RenderableType +from rich.syntax import Syntax from rich.text import Text from textual.app import App from textual.widget import Widget +from textual.widgets import Static + +CODE = ''' +class Offset(NamedTuple): + """A point defined by x and y coordinates.""" + + x: int = 0 + y: int = 0 + + @property + def is_origin(self) -> bool: + """Check if the point is at the origin (0, 0)""" + return self == (0, 0) + + def __bool__(self) -> bool: + return self != (0, 0) + + def __add__(self, other: object) -> Offset: + if isinstance(other, tuple): + _x, _y = self + x, y = other + return Offset(_x + x, _y + y) + return NotImplemented + + def __sub__(self, other: object) -> Offset: + if isinstance(other, tuple): + _x, _y = self + x, y = other + return Offset(_x - x, _y - y) + return NotImplemented + + def __mul__(self, other: object) -> Offset: + if isinstance(other, (float, int)): + x, y = self + return Offset(int(x * other), int(y * other)) + return NotImplemented +''' lorem = Text.from_markup( @@ -56,9 +94,25 @@ class BasicApp(App): def on_mount(self): """Build layout here.""" self.mount( - header=Widget(), + header=Static( + Align.center( + "[b]This is a [u]Textual[/u] app, running in the terminal", + vertical="middle", + ) + ), content=Widget( - Tweet(TweetBody(), Widget(classes={"button"})), + Tweet( + TweetBody(), + # Widget( + # Widget(classes={"button"}), + # Widget(classes={"button"}), + # classes={"horizontal"}, + # ), + ), + Widget( + Static(Syntax(CODE, "python"), classes={"code"}), + classes={"scrollable"}, + ), Error(), Tweet(TweetBody()), Warning(), diff --git a/sandbox/uber.css b/sandbox/uber.css index 9660f69db..29c9a3f32 100644 --- a/sandbox/uber.css +++ b/sandbox/uber.css @@ -8,6 +8,7 @@ .list-item { height: 8; + min-width: 80; background: dark_blu; padding: 2x; diff --git a/src/textual/app.py b/src/textual/app.py index a70ad8279..c467a6b40 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -9,7 +9,7 @@ import warnings from asyncio import AbstractEventLoop from contextlib import redirect_stdout from time import perf_counter -from typing import Any, Iterable, Type, TYPE_CHECKING +from typing import Any, Iterable, TextIO, Type, TYPE_CHECKING import rich import rich.repr @@ -64,6 +64,7 @@ DEFAULT_COLORS = ColorSystem( success="#6d9f71", accent="#ffa62b", system="#5a4599", + dark_surface="#292929", ) @@ -100,7 +101,9 @@ class App(DOMNode): driver_class (Type[Driver], optional): Driver class, or None to use default. Defaults to None. title (str, optional): Title of the application. Defaults to "Textual Application". """ - self.console = Console(markup=False, highlight=False, emoji=False) + self.console = Console( + file=sys.__stdout__, markup=False, highlight=False, emoji=False + ) self.error_console = Console(markup=False, stderr=True) self._screen = screen self.driver_class = driver_class or self.get_driver_class() @@ -122,6 +125,7 @@ class App(DOMNode): self._title = title self._log_console: Console | None = None + self._log_file: TextIO | None = None if log: self._log_file = open(log, "wt") self._log_console = Console( @@ -131,9 +135,6 @@ class App(DOMNode): highlight=False, width=100, ) - else: - self._log_console = None - self._log_file = None self.log_verbosity = log_verbosity @@ -459,6 +460,7 @@ class App(DOMNode): Args: error (Exception): An exception instance. """ + if hasattr(error, "__rich__"): # Exception has a rich method, so we can defer to that for the rendering self.panic(error) @@ -489,15 +491,9 @@ class App(DOMNode): if os.getenv("TEXTUAL_DEVTOOLS") == "1": try: await self.devtools.connect() - if self._log_console: - self._log_console.print( - f"Connected to devtools ({self.devtools.url})" - ) + self.log(f"Connected to devtools ({self.devtools.url})") except DevtoolsConnectionError: - if self._log_console: - self._log_console.print( - f"Couldn't connect to devtools ({self.devtools.url})" - ) + self.log(f"Couldn't connect to devtools ({self.devtools.url})") try: if self.css_file is not None: self.stylesheet.read(self.css_file) @@ -532,11 +528,8 @@ class App(DOMNode): with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore await super().process_messages() - log("Message processing stopped") - with timer("animator.stop()"): - await self.animator.stop() - with timer("self.close_all()"): - await self.close_all() + await self.animator.stop() + await self.close_all() finally: driver.stop_application_mode() except Exception as error: diff --git a/src/textual/box_model.py b/src/textual/box_model.py new file mode 100644 index 000000000..1dc196164 --- /dev/null +++ b/src/textual/box_model.py @@ -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) diff --git a/src/textual/cli/__init__.py b/src/textual/cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py new file mode 100644 index 000000000..b6b882718 --- /dev/null +++ b/src/textual/cli/cli.py @@ -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() diff --git a/src/textual/color.py b/src/textual/color.py index 9b707b8eb..22949cf8c 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -139,6 +139,7 @@ class Color(NamedTuple): @property def rich_color(self) -> RichColor: """This color encoded in Rich's Color class.""" + # TODO: This isn't cheap as I'd like - cache in a LRUCache ? r, g, b, _a = self return RichColor.from_rgb(r, g, b) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 08588e162..04b970615 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -14,6 +14,8 @@ from typing import Iterable, NamedTuple, TYPE_CHECKING, cast import rich.repr from rich.style import Style +from ._help_text import scalar_help_text +from .. import log from ._help_text import ( spacing_wrong_number_of_values, scalar_help_text, @@ -50,10 +52,14 @@ class ScalarProperty: """Descriptor for getting and setting scalar properties. Scalars are numeric values with a unit, e.g. "50vh".""" def __init__( - self, units: set[Unit] | None = None, percent_unit: Unit = Unit.WIDTH + self, + units: set[Unit] | None = None, + percent_unit: Unit = Unit.WIDTH, + allow_auto: bool = True, ) -> None: self.units: set[Unit] = units or {*UNIT_SYMBOL} self.percent_unit = percent_unit + self.allow_auto = allow_auto super().__init__() def __set_name__(self, owner: Styles, name: str) -> None: @@ -96,7 +102,7 @@ class ScalarProperty: obj.clear_rule(self.name) obj.refresh(layout=True) return - if isinstance(value, float) or isinstance(value, int): + if isinstance(value, (int, float)): new_value = Scalar(float(value), Unit.CELLS, Unit.WIDTH) elif isinstance(value, Scalar): new_value = value @@ -112,12 +118,23 @@ class ScalarProperty: ) else: raise StyleValueError("expected float, int, Scalar, or None") - if new_value is not None and new_value.unit 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 ( + new_value is not None + and new_value.unit == Unit.AUTO + and not self.allow_auto + ): + raise StyleValueError("'auto' not allowed here") + + if new_value.unit != Unit.AUTO: + if new_value is not None and new_value.unit not in self.units: + raise StyleValueError( + f"{self.name} units must be one of {friendly_list(get_symbols(self.units))}" + ) + if new_value is not None and new_value.is_percent: + new_value = Scalar( + float(new_value.value), self.percent_unit, Unit.WIDTH + ) if obj.set_rule(self.name, new_value): obj.refresh(layout=True) diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 34680c996..42a914056 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -14,6 +14,8 @@ from ._help_text import ( color_property_help_text, ) from .constants import ( + VALID_ALIGN_HORIZONTAL, + VALID_ALIGN_VERTICAL, VALID_BORDER, VALID_BOX_SIZING, VALID_EDGE, @@ -654,3 +656,32 @@ class StylesBuilder: transitions[css_property] = Transition(duration, easing, delay) self.styles._rules["transitions"] = transitions + + def process_align(self, name: str, tokens: list[Token]) -> None: + if len(tokens) != 2: + self.error(name, tokens[0], "expected two tokens") + token_horizontal = tokens[0] + token_vertical = tokens[1] + if token_horizontal.name != "token": + self.error( + name, + token_horizontal, + f"invalid token {token_horizontal!r}, expected {friendly_list(VALID_ALIGN_HORIZONTAL)}", + ) + if token_vertical.name != "token": + self.error( + name, + token_vertical, + f"invalid token {token_vertical!r}, expected {friendly_list(VALID_ALIGN_VERTICAL)}", + ) + + self.styles._rules["align_horizontal"] = token_horizontal.value + self.styles._rules["align_vertical"] = token_vertical.value + + def process_align_horizontal(self, name: str, tokens: list[Token]) -> None: + value = self._process_enum(name, tokens, VALID_ALIGN_HORIZONTAL) + self.styles._rules["align_horizontal"] = value + + def process_align_vertical(self, name: str, tokens: list[Token]) -> None: + value = self._process_enum(name, tokens, VALID_ALIGN_VERTICAL) + self.styles._rules["align_vertical"] = value diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 17abc9525..b3e231bed 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -29,6 +29,7 @@ VALID_LAYOUT: Final = {"dock", "vertical", "grid"} VALID_BOX_SIZING: Final = {"border-box", "content-box"} VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"} - +VALID_ALIGN_HORIZONTAL: Final = {"left", "center", "right"} +VALID_ALIGN_VERTICAL: Final = {"top", "middle", "bottom"} NULL_SPACING: Final = Spacing.all(0) diff --git a/src/textual/css/scalar.py b/src/textual/css/scalar.py index b10043f1a..d92512a24 100644 --- a/src/textual/css/scalar.py +++ b/src/textual/css/scalar.py @@ -3,11 +3,10 @@ from __future__ import annotations from enum import Enum, unique from functools import lru_cache import re -from typing import Iterable, NamedTuple, TYPE_CHECKING +from typing import Callable, Iterable, NamedTuple, TYPE_CHECKING import rich.repr -from textual.css.tokenizer import Token from .. import log from ..geometry import Offset @@ -34,6 +33,7 @@ class Unit(Enum): HEIGHT = 5 VIEW_WIDTH = 6 VIEW_HEIGHT = 7 + AUTO = 8 UNIT_SYMBOL = { @@ -107,6 +107,10 @@ class Scalar(NamedTuple): def symbol(self) -> str: return UNIT_SYMBOL[self.unit] + @property + def is_auto(self) -> bool: + return self.unit == Unit.AUTO + @classmethod def from_number(cls, value: float) -> Scalar: return cls(float(value), Unit.CELLS, Unit.WIDTH) @@ -124,11 +128,14 @@ class Scalar(NamedTuple): Returns: Scalar: New scalar """ - match = _MATCH_SCALAR(token) - 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) + if token.lower() == "auto": + scalar = cls(1.0, Unit.AUTO, Unit.AUTO) + else: + match = _MATCH_SCALAR(token) + if match is None: + raise ScalarParseError(f"{token!r} is not a valid scalar") + value, unit_name = match.groups() + scalar = cls(float(value), SYMBOL_UNIT[unit_name or ""], percent_unit) return scalar @lru_cache(maxsize=4096) @@ -142,12 +149,13 @@ class Scalar(NamedTuple): viewport (tuple[int, int]): Size of the viewport (typically terminal size) Raises: - ScalarResolveError: _description_ + ScalarResolveError: If the unit is unknown. Returns: - float: _description_ + int: A size (in cells) """ value, unit, percent_unit = self + if unit == Unit.PERCENT: unit = percent_unit try: diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 34fcd2fc1..ae30dc840 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -13,7 +13,7 @@ from rich.style import Style from .. import log from .._animator import Animation, EasingFunction from ..color import Color -from ..geometry import Size, Spacing +from ..geometry import Offset, Size, Spacing from ._style_properties import ( BorderProperty, BoxProperty, @@ -32,17 +32,28 @@ from ._style_properties import ( TransitionsProperty, FractionalProperty, ) -from .constants import VALID_BOX_SIZING, VALID_DISPLAY, VALID_VISIBILITY, VALID_OVERFLOW +from .constants import ( + VALID_ALIGN_HORIZONTAL, + VALID_ALIGN_VERTICAL, + VALID_BOX_SIZING, + VALID_DISPLAY, + VALID_VISIBILITY, + VALID_OVERFLOW, +) from .scalar import Scalar, ScalarOffset, Unit from .scalar_animation import ScalarAnimation from .transition import Transition from .types import ( BoxSizing, Display, + AlignHorizontal, + AlignVertical, Edge, + AlignHorizontal, Overflow, Specificity3, Specificity4, + AlignVertical, Visibility, ) @@ -115,8 +126,12 @@ class RulesMap(TypedDict, total=False): scrollbar_background_hover: Color scrollbar_background_active: Color + align_horizontal: AlignHorizontal + align_vertical: AlignVertical + RULE_NAMES = list(RulesMap.__annotations__.keys()) +RULE_NAMES_SET = frozenset(RULE_NAMES) _rule_getter = attrgetter(*RULE_NAMES) @@ -178,10 +193,10 @@ class StylesBase(ABC): box_sizing = StringEnumProperty(VALID_BOX_SIZING, "border-box", layout=True) width = ScalarProperty(percent_unit=Unit.WIDTH) height = ScalarProperty(percent_unit=Unit.HEIGHT) - min_width = ScalarProperty(percent_unit=Unit.WIDTH) - min_height = ScalarProperty(percent_unit=Unit.HEIGHT) - max_width = ScalarProperty(percent_unit=Unit.WIDTH) - max_height = ScalarProperty(percent_unit=Unit.HEIGHT) + min_width = ScalarProperty(percent_unit=Unit.WIDTH, allow_auto=False) + min_height = ScalarProperty(percent_unit=Unit.HEIGHT, allow_auto=False) + max_width = ScalarProperty(percent_unit=Unit.WIDTH, allow_auto=False) + max_height = ScalarProperty(percent_unit=Unit.HEIGHT, allow_auto=False) dock = DockProperty() docks = DocksProperty() @@ -203,6 +218,9 @@ class StylesBase(ABC): scrollbar_background_hover = ColorProperty("#444444") scrollbar_background_active = ColorProperty("black") + align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left") + align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") + def __eq__(self, styles: object) -> bool: """Check that Styles containts the same rules.""" if not isinstance(styles, StylesBase): @@ -344,70 +362,43 @@ class StylesBase(ABC): else: return None - def get_box_model( - self, container_size: Size, parent_size: Size - ) -> tuple[Size, Spacing]: - """Resolve the box model for this Styles. + def align_width(self, width: int, parent_width: int) -> int: + """Align the width dimension. Args: - container_size (Size): The size of the widget container. - parent_size (Size): The size widget's parent. + width (int): Width of the content. + parent_width (int): Width of the parent container. Returns: - tuple[Size, Spacing]: A tuple with the size of the content area and margin. + int: An offset to add to the X coordinate. """ - has_rule = self.has_rule - width, height = container_size + offset_x = 0 + 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"): - width = self.width.resolve_dimension(container_size, parent_size) - else: - width = max(0, width - self.margin.width) + def align_height(self, height: int, parent_height: int) -> int: + """Align the height dimensions - if self.min_width: - min_width = self.min_width.resolve_dimension(container_size, parent_size) - width = max(width, min_width) + Args: + height (int): Height of the content. + parent_height (int): Height of the parent container. - if self.max_width: - max_width = self.max_width.resolve_dimension(container_size, parent_size) - width = min(width, max_width) - - if has_rule("height"): - height = self.height.resolve_dimension(container_size, parent_size) - else: - height = max(0, height - self.margin.height) - - if self.min_height: - min_height = self.min_height.resolve_dimension(container_size, parent_size) - height = max(height, min_height) - - if self.max_height: - max_height = self.max_height.resolve_dimension(container_size, parent_size) - height = min(width, max_height) - - # TODO: box sizing - - size = Size(width, height) - margin = Spacing(0, 0, 0, 0) - - if self.box_sizing == "content-box": - - if has_rule("padding"): - size += self.padding.totals - if has_rule("border"): - size += self.border.spacing.totals - if has_rule("margin"): - margin = self.margin - - else: # border-box - if has_rule("padding"): - size -= self.padding.totals - if has_rule("border"): - size -= self.border.spacing.totals - if has_rule("margin"): - margin = self.margin - - return size, margin + Returns: + int: An offset to add to the Y coordinate. + """ + offset_y = 0 + align_vertical = self.align_vertical + if align_vertical != "top": + if align_vertical == "middle": + offset_y = (parent_height - height) // 2 + else: + offset_y = parent_height - height + return offset_y @rich.repr.auto @@ -425,6 +416,7 @@ class Styles(StylesBase): return Styles(node=self.node, _rules=self.get_rules(), important=self.important) def has_rule(self, rule: str) -> bool: + assert rule in RULE_NAMES_SET, f"no such rule {rule!r}" return rule in self._rules def clear_rule(self, rule: str) -> bool: @@ -675,6 +667,15 @@ class Styles(StylesBase): ), ) + if has_rule("align_horizontal") and has_rule("align_vertical"): + append_declaration( + "align", f"{self.align_horizontal} {self.align_vertical}" + ) + elif has_rule("align_horizontal"): + append_declaration("align-horizontal", self.align_horizontal) + elif has_rule("align_horizontal"): + append_declaration("align-vertical", self.align_vertical) + lines.sort() return lines diff --git a/src/textual/css/types.py b/src/textual/css/types.py index a74ceb75f..a9f2db433 100644 --- a/src/textual/css/types.py +++ b/src/textual/css/types.py @@ -30,6 +30,8 @@ EdgeType = Literal[ ] Visibility = Literal["visible", "hidden", "initial", "inherit"] Display = Literal["block", "none"] +AlignHorizontal = Literal["left", "center", "right"] +AlignVertical = Literal["top", "middle", "bottom"] BoxSizing = Literal["border-box", "content-box"] Overflow = Literal["scroll", "hidden", "auto"] EdgeStyle = Tuple[str, Color] diff --git a/src/textual/devtools/client.py b/src/textual/devtools/client.py index 7ec767814..689abb96b 100644 --- a/src/textual/devtools/client.py +++ b/src/textual/devtools/client.py @@ -16,7 +16,7 @@ from rich.console import Console from rich.segment import Segment -DEFAULT_PORT = 8081 +DEVTOOLS_PORT = 8081 WEBSOCKET_CONNECT_TIMEOUT = 3 LOG_QUEUE_MAXSIZE = 512 @@ -90,7 +90,7 @@ class DevtoolsClient: port (int): The port the devtools server is accessed via, defaults to 8081 """ - def __init__(self, host: str = "127.0.0.1", port: int = DEFAULT_PORT) -> None: + def __init__(self, host: str = "127.0.0.1", port: int = DEVTOOLS_PORT) -> None: self.url: str = f"ws://{host}:{port}" self.session: aiohttp.ClientSession | None = None self.log_queue_task: Task | None = None diff --git a/src/textual/devtools/renderables.py b/src/textual/devtools/renderables.py index 77cc5df0b..1f08c50c2 100644 --- a/src/textual/devtools/renderables.py +++ b/src/textual/devtools/renderables.py @@ -5,6 +5,8 @@ from datetime import datetime, timezone from pathlib import Path from typing import Iterable +from importlib_metadata import version +from rich.containers import Renderables from rich.style import Style from rich.text import Text @@ -21,10 +23,31 @@ from rich.rule import Rule from rich.segment import Segment, Segments from rich.table import Table -DevtoolsMessageLevel = Literal["info", "warning", "error"] +DevConsoleMessageLevel = Literal["info", "warning", "error"] -class DevtoolsLogMessage: +class DevConsoleHeader: + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + lines = Renderables( + [ + f"[bold]Textual Development Console [magenta]v{version('textual')}", + "[magenta]Run a Textual app with the environment variable [b]TEXTUAL_DEVTOOLS=1[/] to connect.", + "[magenta]Press [b]Ctrl+C[/] to quit.", + ] + ) + render_options = options.update(width=options.max_width - 4) + lines = console.render_lines(lines, render_options) + new_line = Segment("\n") + padding = Segment("▌", Style.parse("bright_magenta")) + for line in lines: + yield padding + yield from line + yield new_line + + +class DevConsoleLog: """Renderable representing a single log message Args: @@ -61,18 +84,17 @@ class DevtoolsLogMessage: file_link = escape(f"file://{Path(self.path).absolute()}") file_and_line = escape(f"{Path(self.path).name}:{self.line_number}") table.add_row( - f" [#888177]{local_time.time()} [dim]{timezone_name}[/]", + f"[dim]{local_time.time()} {timezone_name}", Align.right( - Text(f"{file_and_line} ", style=Style(color="#888177", link=file_link)) + Text(f"{file_and_line}", style=Style(dim=True, link=file_link)) ), - style="on #292724", ) yield table yield Segments(self.segments) -class DevtoolsInternalMessage: - """Renderable for messages written by the devtools server itself +class DevConsoleNotice: + """Renderable for messages written by the devtools console itself Args: message (str): The message to display @@ -80,7 +102,7 @@ class DevtoolsInternalMessage: Determines colors used to render the message and the perceived importance. """ - def __init__(self, message: str, *, level: DevtoolsMessageLevel = "info") -> None: + def __init__(self, message: str, *, level: DevConsoleMessageLevel = "info") -> None: self.message = message self.level = level @@ -89,7 +111,7 @@ class DevtoolsInternalMessage: ) -> RenderResult: level_to_style = { "info": "dim", - "warning": "#FFA000", - "error": "#C52828", + "warning": "yellow", + "error": "red", } yield Rule(self.message, style=level_to_style.get(self.level, "dim")) diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py index 7f0c31839..a0ad3a149 100644 --- a/src/textual/devtools/server.py +++ b/src/textual/devtools/server.py @@ -1,14 +1,12 @@ from __future__ import annotations -import sys - from aiohttp.web import run_app from aiohttp.web_app import Application from aiohttp.web_request import Request from aiohttp.web_routedef import get from aiohttp.web_ws import WebSocketResponse -from textual.devtools.client import DEFAULT_PORT +from textual.devtools.client import DEVTOOLS_PORT from textual.devtools.service import DevtoolsService DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS = 2 @@ -38,9 +36,13 @@ async def _on_startup(app: Application) -> None: await service.start() -def _run_devtools(port: int) -> None: +def _run_devtools() -> None: app = _make_devtools_aiohttp_app() - run_app(app, port=port) + + def noop_print(_: str): + return None + + run_app(app, port=DEVTOOLS_PORT, print=noop_print) def _make_devtools_aiohttp_app( @@ -65,8 +67,4 @@ def _make_devtools_aiohttp_app( if __name__ == "__main__": - if len(sys.argv) > 1: - port = int(sys.argv[1]) - else: - port = DEFAULT_PORT - _run_devtools(port) + _run_devtools() diff --git a/src/textual/devtools/service.py b/src/textual/devtools/service.py index 21f136005..d396fc49d 100644 --- a/src/textual/devtools/service.py +++ b/src/textual/devtools/service.py @@ -14,7 +14,11 @@ from aiohttp.web_ws import WebSocketResponse from rich.console import Console from rich.markup import escape -from textual.devtools.renderables import DevtoolsLogMessage, DevtoolsInternalMessage +from textual.devtools.renderables import ( + DevConsoleLog, + DevConsoleNotice, + DevConsoleHeader, +) QUEUEABLE_TYPES = {"client_log", "client_spillover"} @@ -38,6 +42,7 @@ class DevtoolsService: async def start(self): """Starts devtools tasks""" self.size_poll_task = asyncio.create_task(self._console_size_poller()) + self.console.print(DevConsoleHeader()) @property def clients_connected(self) -> bool: @@ -167,7 +172,7 @@ class ClientHandler: decoded_segments = base64.b64decode(encoded_segments) segments = pickle.loads(decoded_segments) self.service.console.print( - DevtoolsLogMessage( + DevConsoleLog( segments=segments, path=path, line_number=line_number, @@ -176,7 +181,7 @@ class ClientHandler: ) elif type == "client_spillover": spillover = int(message_json["payload"]["spillover"]) - info_renderable = DevtoolsInternalMessage( + info_renderable = DevConsoleNotice( f"Discarded {spillover} messages", level="warning" ) self.service.console.print(info_renderable) @@ -198,9 +203,7 @@ class ClientHandler: if self.request.remote: self.service.console.print( - DevtoolsInternalMessage( - f"Client '{escape(self.request.remote)}' connected" - ) + DevConsoleNotice(f"Client '{escape(self.request.remote)}' connected") ) try: await self.service.send_server_info(client_handler=self) @@ -223,20 +226,16 @@ class ClientHandler: await self.incoming_queue.put(message_json) elif message.type == WSMsgType.ERROR: self.service.console.print( - DevtoolsInternalMessage( - "Websocket error occurred", level="error" - ) + DevConsoleNotice("Websocket error occurred", level="error") ) break except Exception as error: - self.service.console.print( - DevtoolsInternalMessage(str(error), level="error") - ) + self.service.console.print(DevConsoleNotice(str(error), level="error")) finally: if self.request.remote: self.service.console.print( "\n", - DevtoolsInternalMessage( + DevConsoleNotice( f"Client '{escape(self.request.remote)}' disconnected" ), ) diff --git a/src/textual/geometry.py b/src/textual/geometry.py index be617d57f..f71103690 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -6,7 +6,6 @@ Functions and classes to manage terminal geometry (anything involving coordinate from __future__ import annotations -from math import sqrt from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar SpacingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]] @@ -94,7 +93,7 @@ class Offset(NamedTuple): """ x1, y1 = self x2, y2 = other - distance = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) + distance = ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) ** 0.5 return distance @@ -665,5 +664,14 @@ class Spacing(NamedTuple): ) return NotImplemented + def __sub__(self, other: object) -> Spacing: + if isinstance(other, tuple): + top1, right1, bottom1, left1 = self + top2, right2, bottom2, left2 = other + return Spacing( + top1 - top2, right1 - right2, bottom1 - bottom2, left1 - left2 + ) + return NotImplemented + NULL_OFFSET = Offset(0, 0) diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py index e040a9642..b507bbc67 100644 --- a/src/textual/layouts/dock.py +++ b/src/textual/layouts/dock.py @@ -3,6 +3,7 @@ from __future__ import annotations import sys from collections import defaultdict from dataclasses import dataclass +from operator import attrgetter from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence from .._layout_resolve import layout_resolve @@ -91,7 +92,7 @@ class DockLayout(Layout): add_placement = placements.append arranged_widgets: set[Widget] = set() - for edge, widgets, z in docks: + for z, (edge, widgets, _z) in enumerate(sorted(docks, key=attrgetter("z"))): arranged_widgets.update(widgets) dock_options = [make_dock_options(widget, edge) for widget in widgets] diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index db9e3d7b4..36fef9bd7 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import cast from textual.geometry import Size, Offset, Region from textual.layout import Layout, WidgetPlacement @@ -24,15 +25,30 @@ class HorizontalLayout(Layout): x = max_width = max_height = 0 parent_size = parent.size - for widget in parent.children: - (content_width, content_height), margin = widget.styles.get_box_model( - size, parent_size - ) - region = Region(margin.left + x, margin.top, content_width, content_height) - max_height = max(max_height, content_height + margin.height) + box_models = [ + widget.get_box_model(size, parent_size) + for widget in cast("list[Widget]", parent.children) + ] + + margins = [ + max((box1.margin.right, box2.margin.left)) + for box1, box2 in zip(box_models, box_models[1:]) + ] + if box_models: + margins.append(box_models[-1].margin.right) + + x = box_models[0].margin.left if box_models else 0 + + for widget, box_model, margin in zip(parent.children, box_models, margins): + content_width, content_height = box_model.size + offset_y = widget.styles.align_height(content_height, parent_size.height) + region = Region(x, offset_y, content_width, content_height) + max_height = max(max_height, content_height) add_placement(WidgetPlacement(region, widget, 0)) - x += region.width + margin.left - max_width = x + margin.right + x += region.width + margin + max_width = x + + max_width += margins[-1] if margins else 0 total_region = Region(0, 0, max_width, max_height) add_placement(WidgetPlacement(total_region, None, 0)) diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index fca5ed4e1..2752c4445 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import cast, TYPE_CHECKING from .. import log @@ -12,7 +12,7 @@ if TYPE_CHECKING: class VerticalLayout(Layout): - """Simple vertical layout.""" + """Used to layout Widgets vertically on screen, from top to bottom.""" name = "vertical" @@ -26,15 +26,30 @@ class VerticalLayout(Layout): y = max_width = max_height = 0 parent_size = parent.size - for widget in parent.children: - (content_width, content_height), margin = widget.styles.get_box_model( - size, parent_size - ) - region = Region(margin.left, y + margin.top, content_width, content_height) - max_width = max(max_width, content_width + margin.width) + box_models = [ + widget.get_box_model(size, parent_size) + for widget in cast("list[Widget]", parent.children) + ] + + margins = [ + max((box1.margin.bottom, box2.margin.top)) + for box1, box2 in zip(box_models, box_models[1:]) + ] + if box_models: + margins.append(box_models[-1].margin.bottom) + + y = box_models[0].margin.top if box_models else 0 + + for widget, box_model, margin in zip(parent.children, box_models, margins): + content_width, content_height = box_model.size + offset_x = widget.styles.align_width(content_width, parent_size.width) + region = Region(offset_x, y, content_width, content_height) + max_height = max(max_height, content_height) add_placement(WidgetPlacement(region, widget, 0)) - y += region.height + margin.top - max_height = y + margin.bottom + y += region.height + margin + max_height = y + + max_height += margins[-1] if margins else 0 total_region = Region(0, 0, max_width, max_height) add_placement(WidgetPlacement(total_region, None, 0)) diff --git a/src/textual/renderables/gradient.py b/src/textual/renderables/gradient.py index c7eb1a53f..7d5fb3243 100644 --- a/src/textual/renderables/gradient.py +++ b/src/textual/renderables/gradient.py @@ -1,19 +1,19 @@ from __future__ import annotations from rich.console import ConsoleOptions, Console, RenderResult -from rich.color import Color + from rich.segment import Segment from rich.style import Style -from ._blend_colors import blend_colors_rgb +from ..color import Color class VerticalGradient: """Draw a vertical gradient.""" def __init__(self, color1: str, color2: str) -> None: - self._color1 = Color.parse(color1).get_truecolor() - self._color2 = Color.parse(color2).get_truecolor() + self._color1 = Color.parse(color1) + self._color2 = Color.parse(color2) def __rich_console__( self, console: Console, options: ConsoleOptions @@ -22,15 +22,20 @@ class VerticalGradient: height = options.height or options.max_height color1 = self._color1 color2 = self._color2 - default_color = Color.default() + default_color = Color(0, 0, 0).rich_color from_color = Style.from_color + blend = color1.blend + rich_color1 = color1.rich_color for y in range(height): - yield Segment( - f"{width * ' '}\n", - from_color( - default_color, blend_colors_rgb(color1, color2, y / (height - 1)) + line_color = from_color( + default_color, + ( + blend(color2, y / (height - 1)).rich_color + if height > 1 + else rich_color1 ), ) + yield Segment(f"{width * ' '}\n", line_color) if __name__ == "__main__": diff --git a/src/textual/screen.py b/src/textual/screen.py index 0b987352e..e348416fe 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -131,7 +131,8 @@ class Screen(Widget): ) ) except Exception as error: - self.app.panic(error) + self.app.on_exception(error) + return self.app.refresh() self._dirty_widgets.clear() @@ -204,7 +205,11 @@ class Screen(Widget): if isinstance(event, events.MouseDown) and widget.can_focus: await self.app.set_focus(widget) event.style = self.get_style_at(event.screen_x, event.screen_y) - 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)): try: @@ -213,6 +218,9 @@ class Screen(Widget): return scroll_widget = widget 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: await self.post_message(event) diff --git a/src/textual/widget.py b/src/textual/widget.py index 74dc4aabf..817de899a 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -6,7 +6,6 @@ from typing import ( Awaitable, TYPE_CHECKING, Callable, - ClassVar, Iterable, NamedTuple, cast, @@ -15,22 +14,23 @@ from typing import ( import rich.repr from rich.align import Align from rich.console import Console, RenderableType +from rich.measure import Measurement from rich.padding import Padding -from rich.pretty import Pretty from rich.style import Style from rich.styled import Styled -from rich.text import Text + from . import errors, log from . import events from ._animator import BoundAnimator from ._border import Border +from .box_model import BoxModel, get_box_model from ._callback import invoke from .color import Color from ._context import active_app from ._types import Lines from .dom import DOMNode -from .geometry import clamp, Offset, Region, Size +from .geometry import clamp, Offset, Region, Size, Spacing from .message import Message from . import messages from .layout import Layout @@ -96,6 +96,8 @@ class Widget(DOMNode): super().__init__(name=name, id=id, classes=classes) self.add_children(*children) + auto_width = Reactive(True) + auto_height = Reactive(True) has_focus = Reactive(False) mouse_over = Reactive(False) scroll_x = Reactive(0.0, repaint=False) @@ -105,6 +107,34 @@ class Widget(DOMNode): show_vertical_scrollbar = Reactive(False, layout=True) show_horizontal_scrollbar = Reactive(False, layout=True) + def get_box_model(self, container: Size, viewport: Size) -> BoxModel: + """Process the box model for this widget. + + Args: + container (Size): The size of the container widget (with a layout) + viewport (Size): The viewport size. + + Returns: + BoxModel: The size and margin for this widget. + """ + box_model = get_box_model( + self.styles, + container, + viewport, + self.get_content_width, + self.get_content_height, + ) + return box_model + + def get_content_width(self, container_size: Size, parent_size: Size) -> int: + console = self.app.console + renderable = self.render() + measurement = Measurement.get(console, console.options, renderable) + return measurement.maximum + + def get_content_height(self, container_size: Size, parent_size: Size) -> int: + return container_size.height + async def watch_scroll_x(self, new_value: float) -> None: self.horizontal_scrollbar.position = int(new_value) @@ -394,10 +424,7 @@ class Widget(DOMNode): if renderable_text_style: renderable = Styled(renderable, renderable_text_style) - if styles.padding: - renderable = Padding( - renderable, styles.padding, style=renderable_text_style - ) + renderable = Padding(renderable, styles.padding, style=renderable_text_style) if styles.border: renderable = Border( @@ -518,7 +545,9 @@ class Widget(DOMNode): """Render all lines.""" width, height = self.size renderable = self.render_styled() - options = self.console.options.update_dimensions(width, height) + options = self.console.options.update_dimensions(width, height).update( + highlight=False + ) lines = self.console.render_lines(renderable, options) self._render_cache = RenderCache(self.size, lines) self._dirty_regions.clear() @@ -656,13 +685,13 @@ class Widget(DOMNode): def on_mouse_scroll_down(self, event) -> None: if self.is_container: - if not self.scroll_down(animate=False): - event.stop() + self.scroll_down(animate=False) + event.stop() def on_mouse_scroll_up(self, event) -> None: if self.is_container: - if not self.scroll_up(animate=False): - event.stop() + self.scroll_up(animate=False) + event.stop() def handle_scroll_to(self, message: ScrollTo) -> None: if self.is_container: diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py new file mode 100644 index 000000000..3909ba67c --- /dev/null +++ b/tests/cli/test_cli.py @@ -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 diff --git a/tests/devtools/test_devtools.py b/tests/devtools/test_devtools.py index c748da9e6..833c3daf2 100644 --- a/tests/devtools/test_devtools.py +++ b/tests/devtools/test_devtools.py @@ -7,7 +7,7 @@ from rich.console import Console from rich.segment import Segment from tests.utilities.render import wait_for_predicate -from textual.devtools.renderables import DevtoolsLogMessage, DevtoolsInternalMessage +from textual.devtools.renderables import DevConsoleLog, DevConsoleNotice TIMESTAMP = 1649166819 WIDTH = 40 @@ -31,7 +31,7 @@ def console(): @time_machine.travel(TIMESTAMP) def test_log_message_render(console): - message = DevtoolsLogMessage( + message = DevConsoleLog( [Segment("content")], path="abc/hello.py", line_number=123, @@ -56,13 +56,13 @@ def test_log_message_render(console): timezone_name = local_time.tzname() string_timestamp = local_time.time() - assert left == f" [#888177]{string_timestamp} [dim]{timezone_name}[/]" + assert left == f"[dim]{string_timestamp} {timezone_name}" assert right.align == "right" assert "hello.py:123" in right.renderable def test_internal_message_render(console): - message = DevtoolsInternalMessage("hello") + message = DevConsoleNotice("hello") rule = next(iter(message.__rich_console__(console, console.options))) assert rule.title == "hello" assert rule.characters == "─" diff --git a/tests/test_box_model.py b/tests/test_box_model.py new file mode 100644 index 000000000..8d11e56e6 --- /dev/null +++ b/tests/test_box_model.py @@ -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)) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 46f1517c8..923116251 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -317,6 +317,13 @@ def test_spacing_add(): Spacing(1, 2, 3, 4) + "foo" +def test_spacing_sub(): + assert Spacing(1, 2, 3, 4) - Spacing(5, 6, 7, 8) == Spacing(-4, -4, -4, -4) + + with pytest.raises(TypeError): + Spacing(1, 2, 3, 4) - "foo" + + def test_split(): assert Region(10, 5, 22, 15).split(10, 5) == ( Region(10, 5, 10, 5),