diff --git a/CHANGELOG.md b/CHANGELOG.md index 11f0edd6c..3416fd003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.1.15] - 2022-01-31 + +### Added + +- Added Windows Driver + +## [1.1.14] - 2022-01-09 + +### Changed + +- Updated Rich dependency to 11.X ## [0.1.13] - 2022-01-01 diff --git a/README.md b/README.md index 8b3f02849..866cba6f9 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ ![screenshot](./imgs/textual.png) -Textual is a TUI (Text User Interface) framework for Python inspired by modern web development. +Textual is a TUI (Text User Interface) framework for Python inspired by modern web development. Currently a **Work in Progress**. + + +> ⚠ **NOTE:** We ([Textualize.io](https://www.textualize.io)) are hard at work on the **css** branch. We will be maintain the 0.1.0 branch for the near future but may not be able to accept API changes. If you would like to contribute code via a PR, please raise a discussion first, to avoid disapointment. -**NOTE:** This project is currently a work in progress, but usable by brave souls who don't mind some API instability between updates. Follow [@willmcgugan](https://twitter.com/willmcgugan) for progress updates, or post in Discussions if you have any requests / suggestions. @@ -12,7 +14,7 @@ Follow [@willmcgugan](https://twitter.com/willmcgugan) for progress updates, or ## Compatibility -Textual currently runs on **MacOS / Linux only**. Windows support is in the pipeline. +Textual currently runs on **MacOS / Linux / Window**. ## How it works diff --git a/examples/animation.py b/examples/animation.py index 79edd4817..bf5f9ae8e 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -32,5 +32,7 @@ class SmoothApp(App): self.bar.layout_offset_x = -40 + # self.set_timer(10, lambda: self.action("quit")) -SmoothApp.run(log="textual.log") + +SmoothApp.run(log="textual.log", log_verbosity=2) diff --git a/poetry.lock b/poetry.lock index 106226a4b..c58df8a09 100644 --- a/poetry.lock +++ b/poetry.lock @@ -19,32 +19,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.2.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] - -[[package]] -name = "backports.entry-points-selectable" -version = "1.1.0" -description = "Compatibility shim providing selectable entry points for older implementations" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "black" @@ -91,7 +76,7 @@ python-versions = ">=3.6.1" [[package]] name = "click" -version = "8.0.1" +version = "8.0.3" description = "Composable command line interface toolkit" category = "dev" optional = false @@ -122,18 +107,18 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" -version = "5.5" +version = "6.3" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=3.7" [package.extras] -toml = ["toml"] +toml = ["tomli"] [[package]] name = "distlib" -version = "0.3.2" +version = "0.3.4" description = "Distribution utilities" category = "dev" optional = false @@ -141,15 +126,19 @@ python-versions = "*" [[package]] name = "filelock" -version = "3.0.12" +version = "3.4.2" description = "A platform independent file lock." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] [[package]] name = "ghp-import" -version = "2.0.1" +version = "2.0.2" description = "Copy your docs directly to the gh-pages branch." category = "dev" optional = false @@ -159,26 +148,26 @@ python-versions = "*" python-dateutil = ">=2.8.1" [package.extras] -dev = ["twine", "markdown", "flake8"] +dev = ["twine", "markdown", "flake8", "wheel"] [[package]] name = "identify" -version = "2.2.13" +version = "2.4.6" description = "File identification library for Python" category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.extras] -license = ["editdistance-s"] +license = ["ukkonen"] [[package]] name = "importlib-metadata" -version = "4.6.4" +version = "4.10.1" description = "Read metadata from Python packages" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} @@ -187,7 +176,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -199,7 +188,7 @@ python-versions = "*" [[package]] name = "jinja2" -version = "3.0.1" +version = "3.0.3" description = "A very fast and expressive template engine." category = "dev" optional = false @@ -213,14 +202,14 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markdown" -version = "3.3.4" +version = "3.3.6" description = "Python implementation of Markdown." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] testing = ["coverage", "pyyaml"] @@ -278,7 +267,7 @@ mkdocs = ">=1.1,<2.0" [[package]] name = "mkdocs-material" -version = "7.2.5" +version = "7.3.0" description = "A Material Design theme for MkDocs" category = "dev" optional = false @@ -293,14 +282,11 @@ pymdown-extensions = ">=7.0" [[package]] name = "mkdocs-material-extensions" -version = "1.0.1" +version = "1.0.3" description = "Extension pack for Python Markdown." category = "dev" optional = false -python-versions = ">=3.5" - -[package.dependencies] -mkdocs-material = ">=5.0.0" +python-versions = ">=3.6" [[package]] name = "mkdocstrings" @@ -355,14 +341,14 @@ python-versions = "*" [[package]] name = "packaging" -version = "21.0" +version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" @@ -374,11 +360,11 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "platformdirs" -version = "2.2.0" +version = "2.4.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] @@ -386,21 +372,22 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock [[package]] name = "pluggy" -version = "0.13.1" +version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.14.0" +version = "2.17.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -417,15 +404,15 @@ virtualenv = ">=20.0.8" [[package]] name = "py" -version = "1.10.0" +version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pygments" -version = "2.10.0" +version = "2.11.2" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -444,15 +431,18 @@ Markdown = ">=3.2" [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.7" description = "Python parsing module" category = "dev" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "6.2.4" +version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -465,7 +455,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0.0a1" +pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" @@ -517,11 +507,11 @@ numpy-style = ["docstring_parser (>=0.7.3,<0.8.0)"] [[package]] name = "pyyaml" -version = "5.4.1" +version = "6.0" description = "YAML parser and emitter for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.6" [[package]] name = "pyyaml-env-tag" @@ -536,7 +526,7 @@ pyyaml = "*" [[package]] name = "rich" -version = "10.16.1" +version = "11.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false @@ -593,51 +583,50 @@ python-versions = "*" [[package]] name = "virtualenv" -version = "20.7.2" +version = "20.13.0" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -"backports.entry-points-selectable" = ">=1.0.4" distlib = ">=0.3.1,<1" -filelock = ">=3.0.0,<4" +filelock = ">=3.2,<4" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} platformdirs = ">=2,<3" six = ">=1.9.0,<2" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] [[package]] name = "watchdog" -version = "2.1.5" +version = "2.1.6" description = "Filesystem events monitoring" category = "dev" optional = false python-versions = ">=3.6" [package.extras] -watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] +watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "zipp" -version = "3.5.0" +version = "3.7.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.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"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "d9fa84daf9d3ae9839ebb6e846b73749c4c7737bdcf1160257e3b6ecf7086160" +content-hash = "cdb4f091bb4090e971acb3f64c73ee65d099ea7cbf76455ac6e7a99cf6552190" [metadata.files] astunparse = [ @@ -649,12 +638,8 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, -] -"backports.entry-points-selectable" = [ - {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, - {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] black = [ {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, @@ -669,8 +654,8 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -681,89 +666,82 @@ commonmark = [ {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] coverage = [ - {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, - {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, - {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, - {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, - {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, - {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, - {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, - {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, - {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, - {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, - {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, - {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, - {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, - {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, - {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, - {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, - {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, - {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, - {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, - {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, - {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, - {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, - {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, - {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, + {file = "coverage-6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7"}, + {file = "coverage-6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d"}, + {file = "coverage-6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01"}, + {file = "coverage-6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260"}, + {file = "coverage-6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a"}, + {file = "coverage-6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d"}, + {file = "coverage-6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53"}, + {file = "coverage-6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5"}, + {file = "coverage-6.3-cp310-cp310-win32.whl", hash = "sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e"}, + {file = "coverage-6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2"}, + {file = "coverage-6.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30"}, + {file = "coverage-6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92"}, + {file = "coverage-6.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d"}, + {file = "coverage-6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb"}, + {file = "coverage-6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc"}, + {file = "coverage-6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7"}, + {file = "coverage-6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f"}, + {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"}, + {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d"}, + {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318"}, + {file = "coverage-6.3-cp37-cp37m-win32.whl", hash = "sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749"}, + {file = "coverage-6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6"}, + {file = "coverage-6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f"}, + {file = "coverage-6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2"}, + {file = "coverage-6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5"}, + {file = "coverage-6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde"}, + {file = "coverage-6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d"}, + {file = "coverage-6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c"}, + {file = "coverage-6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69"}, + {file = "coverage-6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6"}, + {file = "coverage-6.3-cp38-cp38-win32.whl", hash = "sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89"}, + {file = "coverage-6.3-cp38-cp38-win_amd64.whl", hash = "sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6"}, + {file = "coverage-6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606"}, + {file = "coverage-6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196"}, + {file = "coverage-6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c"}, + {file = "coverage-6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76"}, + {file = "coverage-6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48"}, + {file = "coverage-6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0"}, + {file = "coverage-6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7"}, + {file = "coverage-6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2"}, + {file = "coverage-6.3-cp39-cp39-win32.whl", hash = "sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c"}, + {file = "coverage-6.3-cp39-cp39-win_amd64.whl", hash = "sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d"}, + {file = "coverage-6.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab"}, + {file = "coverage-6.3.tar.gz", hash = "sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099"}, ] distlib = [ - {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, - {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, ] filelock = [ - {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, - {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, + {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, + {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, ] ghp-import = [ - {file = "ghp-import-2.0.1.tar.gz", hash = "sha256:753de2eace6e0f7d4edfb3cce5e3c3b98cd52aadb80163303d1d036bda7b4483"}, + {file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"}, + {file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"}, ] identify = [ - {file = "identify-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"}, - {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"}, + {file = "identify-2.4.6-py2.py3-none-any.whl", hash = "sha256:cf06b1639e0dca0c184b1504d8b73448c99a68e004a80524c7923b95f7b6837c"}, + {file = "identify-2.4.6.tar.gz", hash = "sha256:233679e3f61a02015d4293dbccf16aa0e4996f868bd114688b8c124f18826706"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.6.4-py3-none-any.whl", hash = "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"}, - {file = "importlib_metadata-4.6.4.tar.gz", hash = "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f"}, + {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, + {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] jinja2 = [ - {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, - {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, + {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, + {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, ] markdown = [ - {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, - {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, + {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, + {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, ] markupsafe = [ {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, @@ -814,12 +792,12 @@ mkdocs-autorefs = [ {file = "mkdocs_autorefs-0.2.1-py3-none-any.whl", hash = "sha256:f301b983a34259df90b3fcf7edc234b5e6c7065bd578781e66fd90b8cfbe76be"}, ] mkdocs-material = [ - {file = "mkdocs-material-7.2.5.tar.gz", hash = "sha256:e2a3aa5e20fbdb260d22ec56c01247896c6ae743702e1cd9023fd149a4ae9890"}, - {file = "mkdocs_material-7.2.5-py2.py3-none-any.whl", hash = "sha256:332bafc1584d2d229aa05f7894b4b0f62055fc0d05c96e6ef1785c86ef6e8f91"}, + {file = "mkdocs-material-7.3.0.tar.gz", hash = "sha256:07db0580fa96c3473aee99ec3fb4606a1a5a1e4f4467e64c0cd1ba8da5b6476e"}, + {file = "mkdocs_material-7.3.0-py2.py3-none-any.whl", hash = "sha256:b183c27dc0f44e631bbc32c51057f61a3e2ba8b3c1080e59f944167eeba9ff1d"}, ] mkdocs-material-extensions = [ - {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, - {file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"}, + {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, + {file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"}, ] mkdocstrings = [ {file = "mkdocstrings-0.15.2-py3-none-any.whl", hash = "sha256:8d6cbe64c07ae66739010979ca01d49dd2f64d1a45009f089d217b9cd2a65e36"}, @@ -859,44 +837,44 @@ nodeenv = [ {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] platformdirs = [ - {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"}, - {file = "platformdirs-2.2.0.tar.gz", hash = "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"}, + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, ] pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {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.14.0-py2.py3-none-any.whl", hash = "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4"}, - {file = "pre_commit-2.14.0.tar.gz", hash = "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c"}, + {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"}, ] py = [ - {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, - {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pygments = [ - {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, - {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, + {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, + {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, ] pymdown-extensions = [ {file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"}, {file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"}, ] pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ - {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, - {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, @@ -911,43 +889,47 @@ pytkdocs = [ {file = "pytkdocs-0.11.1.tar.gz", hash = "sha256:1ec7e028fe8361acc1ce909ada4e6beabec28ef31e629618549109e1d58549f0"}, ] pyyaml = [ - {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, - {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, - {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, - {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, - {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, - {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, - {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, - {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, - {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] pyyaml-env-tag = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] rich = [ - {file = "rich-10.16.1-py3-none-any.whl", hash = "sha256:bbe04dd6ac09e4b00d22cb1051aa127beaf6e16c3d8687b026e96d3fca6aad52"}, - {file = "rich-10.16.1.tar.gz", hash = "sha256:4949e73de321784ef6664ebbc854ac82b20ff60b2865097b93f3b9b41e30da27"}, + {file = "rich-11.1.0-py3-none-any.whl", hash = "sha256:365ebcdbfb3aa8d4b0ed2490e0fbf7b886a39d14eb7ea5fb7aece950835e1eed"}, + {file = "rich-11.1.0.tar.gz", hash = "sha256:43e03d8eec12e21beaecc22c828a41c4247356414a12d5879834863d4ad53816"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -999,35 +981,35 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] virtualenv = [ - {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"}, - {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"}, + {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, + {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, ] watchdog = [ - {file = "watchdog-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f57ce4f7e498278fb2a091f39359930144a0f2f90ea8cbf4523c4e25de34028"}, - {file = "watchdog-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b74d0d92a69a7ab5f101f9fe74e44ba017be269efa824337366ccbb4effde85"}, - {file = "watchdog-2.1.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59767f476cd1f48531bf378f0300565d879688c82da8369ca8c52f633299523c"}, - {file = "watchdog-2.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:814d396859c95598f7576d15bc257c3bd3ba61fa4bc1db7dfc18f09070ded7da"}, - {file = "watchdog-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28777dbed3bbd95f9c70f461443990a36c07dbf49ae7cd69932cdd1b8fb2850c"}, - {file = "watchdog-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5cf78f794c9d7bc64a626ef4f71aff88f57a7ae288e0b359a9c6ea711a41395f"}, - {file = "watchdog-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:43bf728eb7830559f329864ab5da2302c15b2efbac24ad84ccc09949ba753c40"}, - {file = "watchdog-2.1.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a7053d4d22dc95c5e0c90aeeae1e4ed5269d2f04001798eec43a654a03008d22"}, - {file = "watchdog-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6f3ad1d973fe8fc8fe64ba38f6a934b74346342fa98ef08ad5da361a05d46044"}, - {file = "watchdog-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41d44ef21a77a32b55ce9bf59b75777063751f688de51098859b7c7f6466589a"}, - {file = "watchdog-2.1.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed4ca4351cd2bb0d863ee737a2011ca44d8d8be19b43509bd4507f8a449b376b"}, - {file = "watchdog-2.1.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8874d5ad6b7f43b18935d9b0183e29727a623a216693d6938d07dfd411ba462f"}, - {file = "watchdog-2.1.5-py3-none-manylinux2014_aarch64.whl", hash = "sha256:50a7f81f99d238f72185f481b493f9de80096e046935b60ea78e1276f3d76960"}, - {file = "watchdog-2.1.5-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e40e33a4889382824846b4baa05634e1365b47c6fa40071dc2d06b4d7c715fc1"}, - {file = "watchdog-2.1.5-py3-none-manylinux2014_i686.whl", hash = "sha256:78b1514067ff4089f4dac930b043a142997a5b98553120919005e97fbaba6546"}, - {file = "watchdog-2.1.5-py3-none-manylinux2014_ppc64.whl", hash = "sha256:58ae842300cbfe5e62fb068c83901abe76e4f413234b7bec5446e4275eb1f9cb"}, - {file = "watchdog-2.1.5-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:b0cc7d8b7d60da6c313779d85903ce39a63d89d866014b085f720a083d5f3e9a"}, - {file = "watchdog-2.1.5-py3-none-manylinux2014_s390x.whl", hash = "sha256:e60d3bb7166b7cb830b86938d1eb0e6cfe23dfd634cce05c128f8f9967895193"}, - {file = "watchdog-2.1.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:51af09ae937ada0e9a10cc16988ec03c649754a91526170b6839b89fc56d6acb"}, - {file = "watchdog-2.1.5-py3-none-win32.whl", hash = "sha256:9391003635aa783957b9b11175d9802d3272ed67e69ef2e3394c0b6d9d24fa9a"}, - {file = "watchdog-2.1.5-py3-none-win_amd64.whl", hash = "sha256:eab14adfc417c2c983fbcb2c73ef3f28ba6990d1fff45d1180bf7e38bda0d98d"}, - {file = "watchdog-2.1.5-py3-none-win_ia64.whl", hash = "sha256:a2888a788893c4ef7e562861ec5433875b7915f930a5a7ed3d32c048158f1be5"}, - {file = "watchdog-2.1.5.tar.gz", hash = "sha256:5563b005907613430ef3d4aaac9c78600dd5704e84764cb6deda4b3d72807f09"}, + {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"}, ] zipp = [ - {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, - {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, + {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, + {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, ] diff --git a/pyproject.toml b/pyproject.toml index fdf571d96..f192f1332 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.1.13" +version = "0.1.15" homepage = "https://github.com/willmcgugan/textual" description = "Text User Interface using Rich" authors = ["Will McGugan "] @@ -19,7 +19,8 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" -rich = "^10.12.0" +rich = "^11.0.0" + #rich = {git = "git@github.com:willmcgugan/rich", rev = "link-id"} typing-extensions = { version = "^3.10.0", python = "<3.8" } diff --git a/src/textual/_box.py b/src/textual/_box.py index 83b2bc2ae..aed0c0153 100644 --- a/src/textual/_box.py +++ b/src/textual/_box.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Literal + from rich.console import Console, ConsoleOptions, RenderResult, RenderableType from rich.segment import Segment @@ -14,6 +16,8 @@ BOX_STYLES: dict[str, tuple[str, str, str]] = { "outer": ("▛▀▜", "▌ ▐", "▙▄▟"), } +BoxType = Literal["", "rounded", "solid", "double", "dashed", "heavy", "inner", "outer"] + class Box: def __init__( diff --git a/src/textual/app.py b/src/textual/app.py index dc110596a..650df6924 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2,7 +2,7 @@ from __future__ import annotations import os import asyncio - +import platform import warnings from typing import Any, Callable, Iterable, Type, TypeVar @@ -21,7 +21,6 @@ from ._animator import Animator from ._callback import invoke from ._context import active_app from ._event_broker import extract_handler_actions, NoHandler -from ._linux_driver import LinuxDriver from ._profile import timer from .binding import Bindings, NoBinding from .css.stylesheet import Stylesheet, StylesheetParseError, StylesheetError @@ -36,6 +35,9 @@ from .view import View from .views import DockView from .widget import Widget +PLATFORM = platform.system() +WINDOWS = PLATFORM == "Windows" + # asyncio will warn against resources not being cleared warnings.simplefilter("always", ResourceWarning) @@ -67,7 +69,6 @@ class App(DOMNode): def __init__( self, - console: Console | None = None, screen: bool = True, driver_class: Type[Driver] | None = None, log: str = "", @@ -85,10 +86,10 @@ 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 or Console() + self.console = Console() self.error_console = Console(stderr=True) self._screen = screen - self.driver_class = driver_class or LinuxDriver + self.driver_class = driver_class or self.get_driver_class() self._title = title self._view_stack: list[View] = [] @@ -131,6 +132,24 @@ class App(DOMNode): sub_title: Reactive[str] = Reactive("") background: Reactive[str] = Reactive("black") + def get_driver_class(self) -> Type[Driver]: + """Get a driver class for this platform. + + Called by the constructor. + + Returns: + Driver: A Driver class which manages input and display. + """ + if WINDOWS: + from .drivers.windows_driver import WindowsDriver + + driver_class = WindowsDriver + else: + from .drivers.linux_driver import LinuxDriver + + driver_class = LinuxDriver + return driver_class + def __rich_repr__(self) -> rich.repr.Result: yield "title", self.title @@ -211,7 +230,7 @@ class App(DOMNode): """ async def run_app() -> None: - app = cls(console=console, screen=screen, driver_class=driver, **kwargs) + app = cls(screen=screen, driver_class=driver, **kwargs) await app.process_messages() asyncio.run(run_app()) @@ -359,6 +378,7 @@ class App(DOMNode): mount_event = events.Mount(sender=self) await self.dispatch_message(mount_event) + self.console = Console() self.title = self._title self.refresh() await self.animator.start() @@ -432,7 +452,9 @@ class App(DOMNode): await self.close_messages() def refresh(self) -> None: - sync_available = os.environ.get("TERM_PROGRAM", "") != "Apple_Terminal" + sync_available = ( + os.environ.get("TERM_PROGRAM", "") != "Apple_Terminal" and not WINDOWS + ) if not self._closed: console = self.console try: diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index ff673fa10..172ca7e14 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -1,5 +1,5 @@ """ -Style properties are descriptors which allow the Styles object to accept different types when +Style properties are descriptors which allow the ``Styles`` object to accept different types when setting attributes. This gives the developer more freedom in how to express style information. Descriptors also play nicely with Mypy, which is aware that attributes can have different types @@ -33,10 +33,13 @@ if TYPE_CHECKING: from ..layout import Layout from .styles import Styles from .styles import DockGroup + from .._box import BoxType from ..layouts.factory import LayoutName 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 ) -> None: @@ -51,12 +54,34 @@ class ScalarProperty: def __get__( self, obj: Styles, objtype: type[Styles] | None = None ) -> Scalar | None: + """Get the scalar property + + Args: + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class + + Returns: + The Scalar object or ``None`` if it's not set. + """ value = getattr(obj, self.internal_name) return value - def __set__( - self, obj: Styles, value: float | Scalar | str | None - ) -> float | Scalar | str | None: + def __set__(self, obj: Styles, value: float | Scalar | str | None) -> None: + """Set the scalar property + + Args: + obj (Styles): The ``Styles`` object. + value (float | Scalar | str | None): The value to set the scalar property to. + You can directly pass a float value, which will be interpreted with + a default unit of Cells. You may also provide a string such as ``"50%"``, + as you might do when writing CSS. If a string with no units is supplied, + Cells will be used as the unit. Alternatively, you can directly supply + a ``Scalar`` object. + + Raises: + StyleValueError: If the value is of an invalid type, uses an invalid unit, or + cannot be parsed for any other reason. + """ if value is None: new_value = None elif isinstance(value, float): @@ -78,10 +103,13 @@ class ScalarProperty: new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH) setattr(obj, self.internal_name, new_value) obj.refresh() - return value class BoxProperty: + """Descriptor for getting and setting outlines and borders along a single edge. + For example "border-right", "outline-bottom", etc. + """ + DEFAULT = ("", Style()) def __set_name__(self, owner: Styles, name: str) -> None: @@ -92,13 +120,32 @@ class BoxProperty: def __get__( self, obj: Styles, objtype: type[Styles] | None = None - ) -> tuple[str, Style]: + ) -> tuple[BoxType, Style]: + """Get the box property + + Args: + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class + + Returns: + A ``tuple[BoxType, Style]`` containing the string type of the box and + it's style. Example types are "rounded", "solid", and "dashed". + """ value = getattr(obj, self.internal_name) return value or self.DEFAULT - def __set__( - self, obj: Styles, border: tuple[str, str | Color | Style] | None - ) -> tuple[str, str | Color | Style] | None: + def __set__(self, obj: Styles, border: tuple[BoxType, str | Color | Style] | None): + """Set the box property + + Args: + obj (Styles): The ``Styles`` object. + value (tuple[BoxType, str | Color | Style], optional): A 2-tuple containing the type of box to use, + e.g. "dashed", and the ``Style`` to be used. You can supply the ``Style`` directly, or pass a + ``str`` (e.g. ``"blue on #f0f0f0"`` ) or ``Color`` instead. + + Raises: + StyleSyntaxError: If the string supplied for the color has invalid syntax. + """ if border is None: new_value = None else: @@ -111,17 +158,16 @@ class BoxProperty: new_value = (_type, Style.from_color(Color.parse(color))) setattr(obj, self.internal_name, new_value) obj.refresh() - return border @rich.repr.auto class Edges(NamedTuple): """Stores edges for border / outline.""" - top: tuple[str, Style] - right: tuple[str, Style] - bottom: tuple[str, Style] - left: tuple[str, Style] + top: tuple[BoxType, Style] + right: tuple[BoxType, Style] + bottom: tuple[BoxType, Style] + left: tuple[BoxType, Style] def __rich_repr__(self) -> rich.repr.Result: top, right, bottom, left = self @@ -150,6 +196,8 @@ class Edges(NamedTuple): class BorderProperty: + """Descriptor for getting and setting full borders and outlines.""" + def __set_name__(self, owner: Styles, name: str) -> None: self._properties = ( f"{name}_top", @@ -159,6 +207,15 @@ class BorderProperty: ) def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Edges: + """Get the border + + Args: + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class + + Returns: + An ``Edges`` object describing the type and style of each edge. + """ top, right, bottom, left = self._properties border = Edges( getattr(obj, top), @@ -171,10 +228,25 @@ class BorderProperty: def __set__( self, obj: Styles, - border: Sequence[tuple[str, str | Color | Style] | None] - | tuple[str, str | Color | Style] + border: Sequence[tuple[BoxType, str | Color | Style] | None] + | tuple[BoxType, str | Color | Style] | None, ) -> None: + """Set the border + + Args: + obj (Styles): The ``Styles`` object. + border (Sequence[tuple[BoxType, str | Color | Style] | None] | tuple[BoxType, str | Color | Style] | None): + A ``tuple[BoxType, str | Color | Style]`` representing the type of box to use and the ``Style`` to apply + to the box. + Alternatively, you can supply a sequence of these tuples and they will be applied per-edge. + If the sequence is of length 1, all edges will be decorated according to the single element. + If the sequence is length 2, the first ``tuple`` will be applied to the top and bottom edges. + If the sequence is length 4, the tuples will be applied to the edges in the order: top, right, bottom, left. + + Raises: + StyleValueError: When the supplied ``tuple`` is not of valid length (1, 2, or 4). + """ top, right, bottom, left = self._properties obj.refresh() if border is None: @@ -213,16 +285,25 @@ class BorderProperty: class StyleProperty: + """Descriptor for getting and setting full borders and outlines.""" + DEFAULT_STYLE = Style() def __set_name__(self, owner: Styles, name: str) -> None: - self._color_name = f"_rule_{name}_color" self._bgcolor_name = f"_rule_{name}_background" self._style_name = f"_rule_{name}_style" def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style: + """Get the Style + Args: + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class + + Returns: + A ``Style`` object. + """ color = getattr(obj, self._color_name) bgcolor = getattr(obj, self._bgcolor_name) style = Style.from_color(color, bgcolor) @@ -231,7 +312,17 @@ class StyleProperty: style += style_flags return style - def __set__(self, obj: Styles, style: Style | str | None) -> Style | str | None: + def __set__(self, obj: Styles, style: Style | str | None): + """Set the Style + + Args: + obj (Styles): The ``Styles`` object. + style (Style | str, optional): You can supply the ``Style`` directly, or a + string (e.g. ``"blue on #f0f0f0"``). + + Raises: + StyleSyntaxError: When the supplied style string has invalid syntax. + """ obj.refresh() if style is None: setattr(obj, self._color_name, None) @@ -246,48 +337,104 @@ class StyleProperty: setattr(obj, self._color_name, new_style.color) setattr(obj, self._bgcolor_name, new_style.bgcolor) setattr(obj, self._style_name, new_style.without_color) - return style class SpacingProperty: + """Descriptor for getting and setting spacing properties (e.g. padding and margin).""" + def __set_name__(self, owner: Styles, name: str) -> None: self._internal_name = f"_rule_{name}" def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Spacing: + """Get the Spacing + + Args: + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class + + Returns: + Spacing: The Spacing. If unset, returns the null spacing ``(0, 0, 0, 0)``. + """ return getattr(obj, self._internal_name) or NULL_SPACING - def __set__(self, obj: Styles, spacing: SpacingDimensions) -> Spacing: + def __set__(self, obj: Styles, spacing: SpacingDimensions): + """Set the Spacing + + Args: + obj (Styles): The ``Styles`` object. + style (Style | str, optional): You can supply the ``Style`` directly, or a + string (e.g. ``"blue on #f0f0f0"``). + + Raises: + ValueError: When the value is malformed, e.g. a ``tuple`` with a length that is + not 1, 2, or 4. + """ obj.refresh(layout=True) spacing = Spacing.unpack(spacing) setattr(obj, self._internal_name, spacing) - return spacing class DocksProperty: + """Descriptor for getting and setting the docks property. This property + is used to define docks and their location on screen. + """ + def __get__( self, obj: Styles, objtype: type[Styles] | None = None ) -> tuple[DockGroup, ...]: + """Get the Docks property + + Args: + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. + + Returns: + tuple[DockGroup, ...]: A ``tuple`` containing the defined docks. + """ return obj._rule_docks or () - def __set__( - self, obj: Styles, docks: Iterable[DockGroup] | None - ) -> Iterable[DockGroup] | None: + def __set__(self, obj: Styles, docks: Iterable[DockGroup] | None): + """Set the Docks property + + Args: + obj (Styles): The ``Styles`` object. + docks (Iterable[DockGroup]): Iterable of DockGroups + """ obj.refresh(layout=True) if docks is None: obj._rule_docks = None else: obj._rule_docks = tuple(docks) - return docks class DockProperty: + """Descriptor for getting and setting the dock property. The dock property + allows you to specify which dock you wish a Widget to be attached to. This + should be used in conjunction with the "docks" property which lets you define + the docks themselves, and where they are located on screen. + """ + def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str: + """Get the Dock property + + Args: + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. + + Returns: + str: The dock name as a string, or "" if the rule is not set. + """ return obj._rule_dock or "" - def __set__(self, obj: Styles, spacing: str | None) -> str | None: + def __set__(self, obj: Styles, spacing: str | None): + """Set the Dock property + + Args: + obj (Styles): The ``Styles`` object + spacing (str | None): The spacing to use. + """ obj.refresh(layout=True) obj._rule_dock = spacing - return spacing class LayoutProperty: @@ -324,17 +471,43 @@ class LayoutProperty: class OffsetProperty: + """Descriptor for getting and setting the offset property. + Offset consists of two values, x and y, that a widget's position + will be adjusted by before it is rendered. + """ + def __set_name__(self, owner: Styles, name: str) -> None: self._internal_name = f"_rule_{name}" def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> ScalarOffset: + """Get the offset + + Args: + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. + + Returns: + ScalarOffset: The ``ScalarOffset`` indicating the adjustment that + will be made to widget position prior to it being rendered. + """ return getattr(obj, self._internal_name) or ScalarOffset( Scalar.from_number(0), Scalar.from_number(0) ) - def __set__( - self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset - ) -> tuple[int | str, int | str] | ScalarOffset: + def __set__(self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset): + """Set the offset + + Args: + obj: The ``Styles`` class + offset: A ScalarOffset object, or a 2-tuple of the form ``(x, y)`` indicating + the x and y offsets. When the ``tuple`` form is used, x and y can be specified + as either ``int`` or ``str``. The string format allows you to also specify + any valid scalar unit e.g. ``("0.5vw", "0.5vh")``. + + Raises: + ScalarParseError: If any of the string values supplied in the 2-tuple cannot + be parsed into a Scalar. For example, if you specify an non-existent unit. + """ obj.refresh(layout=True) if isinstance(offset, ScalarOffset): setattr(obj, self._internal_name, offset) @@ -352,26 +525,48 @@ class OffsetProperty: ) _offset = ScalarOffset(scalar_x, scalar_y) setattr(obj, self._internal_name, _offset) - return offset class IntegerProperty: + """Descriptor for getting and setting integer properties""" + def __set_name__(self, owner: Styles, name: str) -> None: self._name = name self._internal_name = f"_{name}" def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> int: + """Get the integer property, or the default ``0`` if not set. + + Args: + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. + + Returns: + int: The integer property value + """ return getattr(obj, self._internal_name, 0) - def __set__(self, obj: Styles, value: int | None) -> int | None: + def __set__(self, obj: Styles, value: int): + """Set the integer property + + Args: + obj: The ``Styles`` object + value: The value to set the integer to + + Raises: + StyleTypeError: If the supplied value is not an integer. + """ obj.refresh() if not isinstance(value, int): - raise StyleTypeError(f"{self._name} must be a str") + raise StyleTypeError(f"{self._name} must be an integer") setattr(obj, self._internal_name, value) - return value -class StringProperty: +class StringEnumProperty: + """Descriptor for getting and setting string properties and ensuring that the set + value belongs in the set of valid values. + """ + def __init__(self, valid_values: set[str], default: str) -> None: self._valid_values = valid_values self._default = default @@ -381,9 +576,27 @@ class StringProperty: self._internal_name = f"_rule_{name}" def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str: + """Get the string property, or the default value if it's not set + + Args: + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. + + Returns: + str: The string property value + """ return getattr(obj, self._internal_name, None) or self._default - def __set__(self, obj: Styles, value: str | None = None) -> str | None: + def __set__(self, obj: Styles, value: str | None = None): + """Set the string property and ensure it is in the set of allowed values. + + Args: + obj (Styles): The ``Styles`` object + value (str, optional): The string value to set the property to. + + Raises: + StyleValueError: If the value is not in the set of valid values. + """ obj.refresh() if value is not None: if value not in self._valid_values: @@ -391,23 +604,41 @@ class StringProperty: f"{self._name} must be one of {friendly_list(self._valid_values)}" ) setattr(obj, self._internal_name, value) - return value class NameProperty: + """Descriptor for getting and setting name properties.""" + def __set_name__(self, owner: Styles, name: str) -> None: self._name = name self._internal_name = f"_rule_{name}" def __get__(self, obj: Styles, objtype: type[Styles] | None) -> str: + """Get the name property + + Args: + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. + + Returns: + str: The name + """ return getattr(obj, self._internal_name) or "" - def __set__(self, obj: Styles, name: str | None) -> str | None: + def __set__(self, obj: Styles, name: str | None): + """Set the name property + + Args: + obj: The ``Styles`` object + name: The name to set the property to + + Raises: + StyleTypeError: If the value is not a ``str``. + """ obj.refresh(layout=True) if not isinstance(name, str): raise StyleTypeError(f"{self._name} must be a str") setattr(obj, self._internal_name, name) - return name class NameListProperty: @@ -436,14 +667,36 @@ class NameListProperty: class ColorProperty: + """Descriptor for getting and setting color properties.""" + def __set_name__(self, owner: Styles, name: str) -> None: self._name = name self._internal_name = f"_rule_{name}" def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Color: + """Get the ``Color``, or ``Color.default()`` if no color is set. + + Args: + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. + + Returns: + Color: The Color + """ return getattr(obj, self._internal_name, None) or Color.default() - def __set__(self, obj: Styles, color: Color | str | None) -> Color | str | None: + def __set__(self, obj: Styles, color: Color | str | None): + """Set the Color + + Args: + obj (Styles): The ``Styles`` object + color (Color | str | None): The color to set. Pass a ``Color`` instance directly, + or pass a ``str`` which will be parsed into a color (e.g. ``"red""``, ``"rgb(20, 50, 80)"``, + ``"#f4e32d"``). + + Raises: + ColorParseError: When the color string is invalid. + """ obj.refresh() if color is None: setattr(self, self._internal_name, None) @@ -453,10 +706,11 @@ class ColorProperty: elif isinstance(color, str): new_color = Color.parse(color) setattr(self, self._internal_name, new_color) - return color class StyleFlagsProperty: + """Descriptor for getting and set style flag properties (e.g. ``bold italic underline``).""" + _VALID_PROPERTIES = { "not", "bold", @@ -475,9 +729,28 @@ class StyleFlagsProperty: self._internal_name = f"_rule_{name}" def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style: + """Get the ``Style`` + + Args: + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. + + Returns: + Style: The ``Style`` object + """ return getattr(obj, self._internal_name, None) or Style.null() - def __set__(self, obj: Styles, style_flags: str | None) -> str | None: + def __set__(self, obj: Styles, style_flags: str | None): + """Set the style using a style flag string + + Args: + obj (Styles): The ``Styles`` object. + style_flags (str, optional): The style flags to set as a string. For example, + ``"bold italic"``. + + Raises: + StyleValueError: If the value is an invalid style flag + """ obj.refresh() if style_flags is None: setattr(self, self._internal_name, None) @@ -486,13 +759,17 @@ class StyleFlagsProperty: valid_word = self._VALID_PROPERTIES.__contains__ for word in words: if not valid_word(word): - raise StyleValueError(f"unknown word {word!r} in style flags") + raise StyleValueError( + f"unknown word {word!r} in style flags, " + f"valid values are {friendly_list(self._VALID_PROPERTIES)}" + ) style = Style.parse(style_flags) setattr(obj, self._internal_name, style) - return style_flags class TransitionsProperty: + """Descriptor for getting transitions properties""" + def __set_name__(self, owner: Styles, name: str) -> None: self._name = name self._internal_name = f"_rule_{name}" @@ -500,4 +777,15 @@ class TransitionsProperty: def __get__( self, obj: Styles, objtype: type[Styles] | None = None ) -> dict[str, Transition]: + """Get a mapping of properties to the the transitions applied to them. + + Args: + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. + + Returns: + dict[str, Transition]: A ``dict`` mapping property names to the ``Transition`` applied to them. + e.g. ``{"offset": Transition(...), ...}``. If no transitions have been set, an empty ``dict`` + is returned. + """ return getattr(obj, self._internal_name, None) or {} diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 51d6035cb..555e710ea 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -20,7 +20,7 @@ from ._style_properties import ( NameListProperty, ScalarProperty, SpacingProperty, - StringProperty, + StringEnumProperty, StyleProperty, StyleFlagsProperty, TransitionsProperty, @@ -34,6 +34,8 @@ from .scalar import Scalar, ScalarOffset, Unit from .scalar_animation import ScalarAnimation from .transition import Transition from .types import Display, Edge, Visibility + + from .types import Specificity3, Specificity4 from .. import log from .._animator import Animation, EasingFunction @@ -97,8 +99,8 @@ class Styles: important: set[str] = field(default_factory=set) - display = StringProperty(VALID_DISPLAY, "block") - visibility = StringProperty(VALID_VISIBILITY, "visible") + display = StringEnumProperty(VALID_DISPLAY, "block") + visibility = StringEnumProperty(VALID_VISIBILITY, "visible") layout = LayoutProperty() text = StyleProperty() diff --git a/src/textual/driver.py b/src/textual/driver.py index ed4df782d..d6b1fd5ba 100644 --- a/src/textual/driver.py +++ b/src/textual/driver.py @@ -14,9 +14,6 @@ if TYPE_CHECKING: from rich.console import Console -WINDOWS = platform.system() == "Windows" - - class Driver(ABC): def __init__(self, console: "Console", target: "MessageTarget") -> None: self.console = console diff --git a/src/textual/drivers/__init__.py b/src/textual/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/textual/_linux_driver.py b/src/textual/drivers/linux_driver.py similarity index 89% rename from src/textual/_linux_driver.py rename to src/textual/drivers/linux_driver.py index a20e19f0e..45db1925a 100644 --- a/src/textual/_linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -14,17 +14,19 @@ from threading import Event, Thread if TYPE_CHECKING: from rich.console import Console -from . import log +from .. import log -from . import events -from .driver import Driver -from .geometry import Size -from ._types import MessageTarget -from ._xterm_parser import XTermParser -from ._profile import timer +from .. import events +from ..driver import Driver +from ..geometry import Size +from .._types import MessageTarget +from .._xterm_parser import XTermParser +from .._profile import timer class LinuxDriver(Driver): + """Powers display and input for Linux / MacOS""" + def __init__(self, console: "Console", target: "MessageTarget") -> None: super().__init__(console, target) self.fileno = sys.stdin.fileno() @@ -155,21 +157,17 @@ class LinuxDriver(Driver): pass def stop_application_mode(self) -> None: + self.disable_input() - with timer("disable_input"): - self.disable_input() + if self.attrs_before is not None: + try: + termios.tcsetattr(self.fileno, termios.TCSANOW, self.attrs_before) + except termios.error: + pass - with timer("tcsetattr"): - if self.attrs_before is not None: - try: - termios.tcsetattr(self.fileno, termios.TCSANOW, self.attrs_before) - except termios.error: - pass - - with timer("set_alt_screen False, show cursor"): - with self.console: - self.console.set_alt_screen(False) - self.console.show_cursor(True) + with self.console: + self.console.set_alt_screen(False) + self.console.show_cursor(True) def run_input_thread(self, loop) -> None: try: @@ -215,11 +213,11 @@ class LinuxDriver(Driver): if __name__ == "__main__": from time import sleep from rich.console import Console - from . import events + from .. import events console = Console() - from .app import App + from ..app import App class MyApp(App): async def on_mount(self, event: events.Mount) -> None: diff --git a/src/textual/drivers/win32.py b/src/textual/drivers/win32.py new file mode 100644 index 000000000..66c31540f --- /dev/null +++ b/src/textual/drivers/win32.py @@ -0,0 +1,289 @@ +import ctypes +import msvcrt +import sys +import threading +from asyncio import AbstractEventLoop, run_coroutine_threadsafe +from ctypes import Structure, Union, byref, wintypes +from ctypes.wintypes import BOOL, CHAR, DWORD, HANDLE, SHORT, UINT, WCHAR, WORD +from typing import IO, Callable, List, Optional + +from .._types import EventTarget +from .._xterm_parser import XTermParser +from ..events import Event, Resize +from ..geometry import Size + +KERNEL32 = ctypes.WinDLL("kernel32", use_last_error=True) + +# Console input modes +ENABLE_ECHO_INPUT = 0x0004 +ENABLE_EXTENDED_FLAGS = 0x0080 +ENABLE_INSERT_MODE = 0x0020 +ENABLE_LINE_INPUT = 0x0002 +ENABLE_MOUSE_INPUT = 0x0010 +ENABLE_PROCESSED_INPUT = 0x0001 +ENABLE_QUICK_EDIT_MODE = 0x0040 +ENABLE_WINDOW_INPUT = 0x0008 +ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 + +# Console output modes +ENABLE_PROCESSED_OUTPUT = 0x0001 +ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 +DISABLE_NEWLINE_AUTO_RETURN = 0x0008 +ENABLE_LVB_GRID_WORLDWIDE = 0x0010 + +STD_INPUT_HANDLE = -10 +STD_OUTPUT_HANDLE = -11 + +WAIT_TIMEOUT = 0x00000102 + +GetStdHandle = KERNEL32.GetStdHandle +GetStdHandle.argtypes = [wintypes.DWORD] +GetStdHandle.restype = wintypes.HANDLE + + +class COORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/coord-str""" + + _fields_ = [ + ("X", SHORT), + ("Y", SHORT), + ] + + +class uChar(Union): + """https://docs.microsoft.com/en-us/windows/console/key-event-record-str""" + + _fields_ = [ + ("AsciiChar", CHAR), + ("UnicodeChar", WCHAR), + ] + + +class KEY_EVENT_RECORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/key-event-record-str""" + + _fields_ = [ + ("bKeyDown", BOOL), + ("wRepeatCount", WORD), + ("wVirtualKeyCode", WORD), + ("wVirtualScanCode", WORD), + ("uChar", uChar), + ("dwControlKeyState", DWORD), + ] + + +class MOUSE_EVENT_RECORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str""" + + _fields_ = [ + ("dwMousePosition", COORD), + ("dwButtonState", DWORD), + ("dwControlKeyState", DWORD), + ("dwEventFlags", DWORD), + ] + + +class WINDOW_BUFFER_SIZE_RECORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/window-buffer-size-record-str""" + + _fields_ = [("dwSize", COORD)] + + +class MENU_EVENT_RECORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/menu-event-record-str""" + + _fields_ = [("dwCommandId", UINT)] + + +class FOCUS_EVENT_RECORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/focus-event-record-str""" + + _fields_ = [("bSetFocus", BOOL)] + + +class InputEvent(Union): + """https://docs.microsoft.com/en-us/windows/console/input-record-str""" + + _fields_ = [ + ("KeyEvent", KEY_EVENT_RECORD), + ("MouseEvent", MOUSE_EVENT_RECORD), + ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), + ("MenuEvent", MENU_EVENT_RECORD), + ("FocusEvent", FOCUS_EVENT_RECORD), + ] + + +class INPUT_RECORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/input-record-str""" + + _fields_ = [("EventType", wintypes.WORD), ("Event", InputEvent)] + + +def _set_console_mode(file: IO, mode: int) -> bool: + """Set the console mode for a given file (stdout or stdin). + + Args: + file (IO): A file like object. + mode (int): New mode. + + Returns: + bool: True on success, otherwise False. + """ + windows_filehandle = msvcrt.get_osfhandle(file.fileno()) + success = KERNEL32.SetConsoleMode(windows_filehandle, mode) + return success + + +def _get_console_mode(file: IO) -> int: + """Get the console mode for a given file (stdout or stdin) + + Args: + file (IO): A file-like object. + + Returns: + int: The current console mode. + """ + windows_filehandle = msvcrt.get_osfhandle(file.fileno()) + mode = wintypes.DWORD() + KERNEL32.GetConsoleMode(windows_filehandle, ctypes.byref(mode)) + return mode.value + + +def enable_application_mode() -> Callable[[], None]: + """Enable application mode. + + Returns: + Callable[[], None]: A callable that will restore terminal to previous state. + """ + + terminal_in = sys.stdin + terminal_out = sys.stdout + + current_console_mode_in = _get_console_mode(terminal_in) + current_console_mode_out = _get_console_mode(terminal_out) + + def restore() -> None: + """Restore console mode to previous settings""" + _set_console_mode(terminal_in, current_console_mode_in) + _set_console_mode(terminal_out, current_console_mode_out) + + _set_console_mode( + terminal_out, current_console_mode_out | ENABLE_VIRTUAL_TERMINAL_PROCESSING + ) + _set_console_mode(terminal_in, ENABLE_VIRTUAL_TERMINAL_INPUT) + return restore + + +def _wait_for_handles(handles: List[HANDLE], timeout: int = -1) -> Optional[HANDLE]: + """ + Waits for multiple handles. (Similar to 'select') Returns the handle which is ready. + Returns `None` on timeout. + http://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx + Note that handles should be a list of `HANDLE` objects, not integers. See + this comment in the patch by @quark-zju for the reason why: + ''' Make sure HANDLE on Windows has a correct size + Previously, the type of various HANDLEs are native Python integer + types. The ctypes library will treat them as 4-byte integer when used + in function arguments. On 64-bit Windows, HANDLE is 8-byte and usually + a small integer. Depending on whether the extra 4 bytes are zero-ed out + or not, things can happen to work, or break. ''' + This function returns either `None` or one of the given `HANDLE` objects. + (The return value can be tested with the `is` operator.) + """ + arrtype = HANDLE * len(handles) + handle_array = arrtype(*handles) + + ret: int = KERNEL32.WaitForMultipleObjects( + len(handle_array), handle_array, BOOL(False), DWORD(timeout) + ) + + if ret == WAIT_TIMEOUT: + return None + else: + return handles[ret] + + +class EventMonitor(threading.Thread): + """A thread to send key / window events to Textual loop.""" + + def __init__( + self, + loop: AbstractEventLoop, + app, + target: EventTarget, + exit_event: threading.Event, + process_event: Callable[[Event], None], + ) -> None: + self.loop = loop + self.app = app + self.target = target + self.exit_event = exit_event + self.process_event = process_event + self.app.log("event monitor constructed") + super().__init__() + + def run(self) -> None: + self.app.log("event monitor thread started") + exit_requested = self.exit_event.is_set + parser = XTermParser(self.target, lambda: False) + + try: + read_count = wintypes.DWORD(0) + hIn = GetStdHandle(STD_INPUT_HANDLE) + + MAX_EVENTS = 1024 + KEY_EVENT = 0x0001 + WINDOW_BUFFER_SIZE_EVENT = 0x0004 + + arrtype = INPUT_RECORD * MAX_EVENTS + input_records = arrtype() + ReadConsoleInputW = KERNEL32.ReadConsoleInputW + keys: List[str] = [] + append_key = keys.append + + while not exit_requested(): + # Wait for new events + if _wait_for_handles([hIn], 200) is None: + # No new events + continue + + # Get new events + ReadConsoleInputW( + hIn, byref(input_records), MAX_EVENTS, byref(read_count) + ) + read_input_records = input_records[: read_count.value] + + del keys[:] + new_size: Optional[tuple[int, int]] = None + + for input_record in read_input_records: + event_type = input_record.EventType + + if event_type == KEY_EVENT: + # Key event, store unicode char in keys list + key_event = input_record.Event.KeyEvent + key = key_event.uChar.UnicodeChar + if key_event.bKeyDown or key == "\x1b": + append_key(key) + elif event_type == WINDOW_BUFFER_SIZE_EVENT: + # Window size changed, store size + size = input_record.Event.WindowBufferSizeEvent.dwSize + new_size = (size.X, size.Y) + + if keys: + # Process keys + for event in parser.feed("".join(keys)): + self.process_event(event) + if new_size is not None: + # Process changed size + self.on_size_change(*new_size) + + except Exception as error: + self.app.log("EVENT MONITOR ERROR", error) + self.app.log("event monitor thread finished") + + def on_size_change(self, width: int, height: int) -> None: + """Called when terminal size changes.""" + event = Resize(self.target, Size(width, height)) + run_coroutine_threadsafe(self.target.post_message(event), loop=self.loop) diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py new file mode 100644 index 000000000..bc7a73c57 --- /dev/null +++ b/src/textual/drivers/windows_driver.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import asyncio +import sys +from threading import Event, Thread +from typing import TYPE_CHECKING, Callable + +from .._context import active_app +from .._types import MessageTarget +from ..driver import Driver +from . import win32 + +if TYPE_CHECKING: + from rich.console import Console + + +class WindowsDriver(Driver): + """Powers display and input for Windows.""" + + def __init__(self, console: "Console", target: "MessageTarget") -> None: + super().__init__(console, target) + self.in_fileno = sys.stdin.fileno() + self.out_fileno = sys.stdout.fileno() + + self.exit_event = Event() + self._event_thread: Thread | None = None + self._restore_console: Callable[[], None] | None = None + + def _enable_mouse_support(self) -> None: + write = self.console.file.write + write("\x1b[?1000h") # SET_VT200_MOUSE + write("\x1b[?1003h") # SET_ANY_EVENT_MOUSE + write("\x1b[?1015h") # SET_VT200_HIGHLIGHT_MOUSE + write("\x1b[?1006h") # SET_SGR_EXT_MODE_MOUSE + self.console.file.flush() + + def _disable_mouse_support(self) -> None: + write = self.console.file.write + write("\x1b[?1000l") + write("\x1b[?1003l") + write("\x1b[?1015l") + write("\x1b[?1006l") + self.console.file.flush() + + def start_application_mode(self) -> None: + + loop = asyncio.get_event_loop() + + self._restore_console = win32.enable_application_mode() + + self.console.set_alt_screen(True) + self._enable_mouse_support() + self.console.show_cursor(False) + self.console.file.write("\033[?1003h\n") + + app = active_app.get() + + self._event_thread = win32.EventMonitor( + loop, app, self._target, self.exit_event, self.process_event + ) + self._event_thread.start() + + def disable_input(self) -> None: + try: + if not self.exit_event.is_set(): + self._disable_mouse_support() + self.exit_event.set() + if self._event_thread is not None: + self._event_thread.join() + self._event_thread = None + except Exception as error: + # TODO: log this + pass + + def stop_application_mode(self) -> None: + self.disable_input() + if self._restore_console: + self._restore_console() + with self.console: + self.console.set_alt_screen(False) + self.console.show_cursor(True) diff --git a/src/textual/events.py b/src/textual/events.py index a4222531c..ec136a7f0 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -237,7 +237,7 @@ class MouseEvent(InputEvent, bubble=True): Args: sender (MessageTarget): The sender of the event. x (int): The relative x coordinate. - y (int): The relative y cootdinate. + y (int): The relative y coordinate. delta_x (int): Change in x since the last message. delta_y (int): Change in y since the last message. button (int): Indexed of the pressed button. diff --git a/src/textual/renderables/__init__.py b/src/textual/renderables/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/textual/renderables/underline_bar.py b/src/textual/renderables/underline_bar.py new file mode 100644 index 000000000..59c9e6bb4 --- /dev/null +++ b/src/textual/renderables/underline_bar.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from rich.console import ConsoleOptions, Console, RenderResult +from rich.segment import Segment +from rich.style import StyleType + + +class UnderlineBar: + """Thin horizontal bar with a portion highlighted. + + Args: + highlight_range (tuple[float, float]): The range to highlight. Defaults to ``(0, 0)`` (no highlight) + highlight_style (StyleType): The style of the highlighted range of the bar. + background_style (StyleType): The style of the non-highlighted range(s) of the bar. + width (int, optional): The width of the bar, or ``None`` to fill available width. + """ + + def __init__( + self, + highlight_range: tuple[float, float] = (0, 0), + highlight_style: StyleType = "magenta", + background_style: StyleType = "grey37", + width: int | None = None, + ) -> None: + self.highlight_range = highlight_range + self.highlight_style = highlight_style + self.background_style = background_style + self.width = width + + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + highlight_style = console.get_style(self.highlight_style) + background_style = console.get_style(self.background_style) + + half_bar_right = "╸" + half_bar_left = "╺" + bar = "━" + + width = self.width or options.max_width + start, end = self.highlight_range + + start = max(start, 0) + end = min(end, width) + + if start == end == 0 or end < 0 or start > end: + yield Segment(bar * width, style=background_style) + return + + # Round start and end to nearest half + start = round(start * 2) / 2 + end = round(end * 2) / 2 + + # Check if we start/end on a number that rounds to a .5 + half_start = start - int(start) > 0 + half_end = end - int(end) > 0 + + # Initial non-highlighted portion of bar + yield Segment(bar * (int(start - 0.5)), style=background_style) + if not half_start and start > 0: + yield Segment(half_bar_right, style=background_style) + + # The highlighted portion + bar_width = int(end) - int(start) + if half_start: + yield Segment(half_bar_left + bar * (bar_width - 1), style=highlight_style) + else: + yield Segment(bar * bar_width, style=highlight_style) + if half_end: + yield Segment(half_bar_right, style=highlight_style) + + # The non-highlighted tail + if not half_end and end - width != 0: + yield Segment(half_bar_left, style=background_style) + yield Segment(bar * (int(width) - int(end) - 1), style=background_style) + + +if __name__ == "__main__": + import random + from time import sleep + from rich.color import ANSI_COLOR_NAMES + + console = Console() + + def frange(start, end, step): + current = start + while current < end: + yield current + current += step + + while current >= 0: + yield current + current -= step + + step = 0.1 + start_range = frange(0.5, 10.5, step) + end_range = frange(10, 20, step) + ranges = zip(start_range, end_range) + + console.print(UnderlineBar(width=20), f" (.0, .0)") + + for range in ranges: + color = random.choice(list(ANSI_COLOR_NAMES.keys())) + console.print( + UnderlineBar( + range, + highlight_style=color, + width=20, + ), + f" {range}", + ) + + from rich.live import Live + + bar = UnderlineBar(width=80, highlight_range=(0, 4.5)) + with Live(bar, refresh_per_second=60) as live: + while True: + bar.highlight_range = ( + bar.highlight_range[0] + 0.1, + bar.highlight_range[1] + 0.1, + ) + sleep(0.005) diff --git a/tests/renderables/__init__.py b/tests/renderables/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/renderables/test_underline_bar.py b/tests/renderables/test_underline_bar.py new file mode 100644 index 000000000..5c5e4de9c --- /dev/null +++ b/tests/renderables/test_underline_bar.py @@ -0,0 +1,126 @@ +from tests.utilities.render import render +from textual.renderables.underline_bar import UnderlineBar + +MAGENTA = "\x1b[35m" +GREY = "\x1b[38;5;59m" +STOP = "\x1b[0m" +GREEN = "\x1b[32m" +RED = "\x1b[31m" + + +def test_no_highlight(): + bar = UnderlineBar(width=6) + assert render(bar) == f"{GREY}━━━━━━{STOP}" + + +def test_highlight_from_zero(): + bar = UnderlineBar(highlight_range=(0, 2.5), width=6) + assert render(bar) == ( + f"{MAGENTA}━━{STOP}{MAGENTA}╸{STOP}{GREY}━━━{STOP}" + ) + + +def test_highlight_from_zero_point_five(): + bar = UnderlineBar(highlight_range=(0.5, 2), width=6) + assert render(bar) == ( + f"{MAGENTA}╺━{STOP}{GREY}╺{STOP}{GREY}━━━{STOP}" + ) + + +def test_highlight_middle(): + bar = UnderlineBar(highlight_range=(2, 4), width=6) + assert render(bar) == ( + f"{GREY}━{STOP}" + f"{GREY}╸{STOP}" + f"{MAGENTA}━━{STOP}" + f"{GREY}╺{STOP}" + f"{GREY}━{STOP}" + ) + + +def test_highlight_half_start(): + bar = UnderlineBar(highlight_range=(2.5, 4), width=6) + assert render(bar) == ( + f"{GREY}━━{STOP}" + f"{MAGENTA}╺━{STOP}" + f"{GREY}╺{STOP}" + f"{GREY}━{STOP}" + ) + + +def test_highlight_half_end(): + bar = UnderlineBar(highlight_range=(2, 4.5), width=6) + assert render(bar) == ( + f"{GREY}━{STOP}" + f"{GREY}╸{STOP}" + f"{MAGENTA}━━{STOP}" + f"{MAGENTA}╸{STOP}" + f"{GREY}━{STOP}" + ) + + +def test_highlight_half_start_and_half_end(): + bar = UnderlineBar(highlight_range=(2.5, 4.5), width=6) + assert render(bar) == ( + f"{GREY}━━{STOP}" + f"{MAGENTA}╺━{STOP}" + f"{MAGENTA}╸{STOP}" + f"{GREY}━{STOP}" + ) + + +def test_highlight_to_near_end(): + bar = UnderlineBar(highlight_range=(3, 5.5), width=6) + assert render(bar) == ( + f"{GREY}━━{STOP}" + f"{GREY}╸{STOP}" + f"{MAGENTA}━━{STOP}" + f"{MAGENTA}╸{STOP}" + ) + + +def test_highlight_to_end(): + bar = UnderlineBar(highlight_range=(3, 6), width=6) + assert render(bar) == ( + f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}" + ) + + +def test_highlight_out_of_bounds_start(): + bar = UnderlineBar(highlight_range=(-2, 3), width=6) + assert render(bar) == ( + f"{MAGENTA}━━━{STOP}{GREY}╺{STOP}{GREY}━━{STOP}" + ) + + +def test_highlight_out_of_bounds_end(): + bar = UnderlineBar(highlight_range=(3, 9), width=6) + assert render(bar) == ( + f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}" + ) + + +def test_highlight_full_range_out_of_bounds_end(): + bar = UnderlineBar(highlight_range=(9, 10), width=6) + assert render(bar) == f"{GREY}━━━━━━{STOP}" + + +def test_highlight_full_range_out_of_bounds_start(): + bar = UnderlineBar(highlight_range=(-5, -2), width=6) + assert render(bar) == f"{GREY}━━━━━━{STOP}" + + +def test_custom_styles(): + bar = UnderlineBar( + highlight_range=(2, 4), + highlight_style="red", + background_style="green", + width=6 + ) + assert render(bar) == ( + f"{GREEN}━{STOP}" + f"{GREEN}╸{STOP}" + f"{RED}━━{STOP}" + f"{GREEN}╺{STOP}" + f"{GREEN}━{STOP}" + ) diff --git a/tests/utilities/render.py b/tests/utilities/render.py new file mode 100644 index 000000000..a2435c542 --- /dev/null +++ b/tests/utilities/render.py @@ -0,0 +1,24 @@ +import io +import re + +from rich.console import Console, RenderableType + + +re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b") + + +def replace_link_ids(render: str) -> str: + """Link IDs have a random ID and system path which is a problem for + reproducible tests. + + """ + return re_link_ids.sub("id=0;foo\x1b", render) + + +def render(renderable: RenderableType, no_wrap: bool = False) -> str: + console = Console( + width=100, file=io.StringIO(), color_system="truecolor", legacy_windows=False + ) + console.print(renderable, no_wrap=no_wrap) + output = replace_link_ids(console.file.getvalue()) + return output