From 3d1a851a82e301f0f6eff53ac0165b6308e2e83e Mon Sep 17 00:00:00 2001 From: Uzzal Hossain Date: Wed, 17 Nov 2021 10:17:37 +0600 Subject: [PATCH 01/27] Fixed a typo --- src/textual/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/events.py b/src/textual/events.py index 02dafafcf..a4222531c 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -66,7 +66,7 @@ class Load(Event, bubble=False): class Idle(Event, bubble=False): """Sent when there are no more items in the message queue. - This is a psuedo-event in that it is created by the Textual system and doesn't go + This is a pseudo-event in that it is created by the Textual system and doesn't go through the usual message queue. """ From 288e70023d1469429ec77bb23d4854a207956724 Mon Sep 17 00:00:00 2001 From: Adrian de Anda Date: Tue, 23 Nov 2021 23:21:06 -0600 Subject: [PATCH 02/27] Fix couple of typos --- src/textual/app.py | 2 +- src/textual/events.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 28335bada..93a471dda 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -394,7 +394,7 @@ class App(MessagePump): async def on_event(self, event: events.Event) -> None: # Handle input events that haven't been forwarded - # If the event has been forwaded it may have bubbled up back to the App + # If the event has been forwarded it may have bubbled up back to the App if isinstance(event, events.InputEvent) and not event.is_forwarded: if isinstance(event, events.MouseEvent): # Record current mouse position on App diff --git a/src/textual/events.py b/src/textual/events.py index 02dafafcf..35c3ef6ce 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. From 2f808973aa1c4815438c5bdbaf9fdbccef929e5b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 9 Jan 2022 15:38:44 +0000 Subject: [PATCH 03/27] version bump --- CHANGELOG.md | 5 + poetry.lock | 442 +++++++++++++++++++++++-------------------------- pyproject.toml | 4 +- 3 files changed, 215 insertions(+), 236 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11f0edd6c..575952148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ 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.14] - 2022-01-09 + +### Changed + +- Updated Rich dependency to 11.X ## [0.1.13] - 2022-01-01 diff --git a/poetry.lock b/poetry.lock index 7a97e0e7b..c6bbec086 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.2" 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.6" [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.2" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.6.1" [package.extras] -license = ["editdistance-s"] +license = ["ukkonen"] [[package]] name = "importlib-metadata" -version = "4.6.4" +version = "4.10.0" 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"] @@ -243,7 +232,7 @@ python-versions = ">=3.6" [[package]] name = "mkdocs" -version = "1.2.2" +version = "1.2.3" description = "Project documentation with Markdown." category = "dev" optional = false @@ -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.16.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.6" 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.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false @@ -583,14 +573,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "typing-extensions" -version = "3.10.0.0" -description = "Backported and Experimental Type Hints for Python 3.5+" -category = "main" -optional = false -python-versions = "*" - [[package]] name = "typing-extensions" version = "3.10.0.2" @@ -601,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 = "4e81046724f8d03d079e07d047f8bb6f14a0889716fac3938647934a0052d834" +content-hash = "cdb4f091bb4090e971acb3f64c73ee65d099ea7cbf76455ac6e7a99cf6552190" [metadata.files] astunparse = [ @@ -657,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"}, @@ -677,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"}, @@ -689,89 +666,85 @@ 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.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, + {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, + {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, + {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, + {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, + {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, + {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, + {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, + {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, + {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, + {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, + {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, + {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, + {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, + {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, + {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, + {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, + {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, + {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, + {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, ] 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.2-py2.py3-none-any.whl", hash = "sha256:67c1e66225870dce721228176637a8ef965e8dd58450bcc7592249d0dfc4da6c"}, + {file = "identify-2.4.2.tar.gz", hash = "sha256:93e8ec965e888f2212aa5c24b2b662f4832c39acb1d7196a70ea45acb626a05e"}, ] 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.0-py3-none-any.whl", hash = "sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"}, + {file = "importlib_metadata-4.10.0.tar.gz", hash = "sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6"}, ] 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,20 +787,20 @@ mergedeep = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] mkdocs = [ - {file = "mkdocs-1.2.2-py3-none-any.whl", hash = "sha256:d019ff8e17ec746afeb54eb9eb4112b5e959597aebc971da46a5c9486137f0ff"}, - {file = "mkdocs-1.2.2.tar.gz", hash = "sha256:a334f5bd98ec960638511366eb8c5abc9c99b9083a0ed2401d8791b112d6b078"}, + {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, + {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, ] mkdocs-autorefs = [ {file = "mkdocs-autorefs-0.2.1.tar.gz", hash = "sha256:b8156d653ed91356e71675ce1fa1186d2b2c2085050012522895c9aa98fca3e5"}, {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"}, @@ -867,44 +840,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.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"}, + {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, ] 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.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] 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"}, @@ -919,43 +892,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.0.0-py3-none-any.whl", hash = "sha256:d7a8086aa1fa7e817e3bba544eee4fd82047ef59036313147759c11475f0dafd"}, + {file = "rich-11.0.0.tar.gz", hash = "sha256:c32a8340b21c75931f157466fefe81ae10b92c36a5ea34524dff3767238774a4"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1002,43 +979,40 @@ typed-ast = [ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, {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 5d2fe9c62..17bd50ac5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.1.13" +version = "0.1.14" homepage = "https://github.com/willmcgugan/textual" description = "Text User Interface using Rich" authors = ["Will McGugan "] @@ -20,7 +20,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" -rich = "^10.7.0" +rich = "^11.0.0" #rich = {git = "git@github.com:willmcgugan/rich", rev = "link-id"} typing-extensions = { version = "^3.10.0", python = "<3.8" } From 7f9b6eaefc2fe16b96217c75e58b3b43e28ffedf Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 17 Jan 2022 13:29:49 +0000 Subject: [PATCH 04/27] windows driver abstraction --- src/textual/_windows_driver.py | 12 ++++++++++++ src/textual/app.py | 22 ++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/textual/_windows_driver.py diff --git a/src/textual/_windows_driver.py b/src/textual/_windows_driver.py new file mode 100644 index 000000000..8d7de90e6 --- /dev/null +++ b/src/textual/_windows_driver.py @@ -0,0 +1,12 @@ +from .driver import Driver + + +class WindowsDriver(Driver): + def start_application_mode(self) -> None: + pass + + def disable_input(self) -> None: + pass + + def stop_application_mode(self) -> None: + pass diff --git a/src/textual/app.py b/src/textual/app.py index 93a471dda..f92e1bfc2 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -3,6 +3,7 @@ import os import asyncio from functools import partial +import platform from typing import Any, Callable, ClassVar, Type, TypeVar import warnings @@ -24,7 +25,6 @@ from ._context import active_app from ._event_broker import extract_handler_actions, NoHandler from .driver import Driver from .layouts.dock import DockLayout, Dock -from ._linux_driver import LinuxDriver from .message_pump import MessagePump from ._profile import timer from .view import View @@ -78,7 +78,7 @@ class App(MessagePump): self.console = console or 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._layout = DockLayout() self._view_stack: list[DockView] = [] @@ -110,6 +110,24 @@ class App(MessagePump): 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 platform.system() == "Windows": + from ._windows_driver import WindowsDriver + + driver_class = WindowsDriver + else: + from ._linux_driver import LinuxDriver + + driver_class = LinuxDriver + return driver_class + def __rich_repr__(self) -> rich.repr.Result: yield "title", self.title From 54e63428644710112215c4f2d27cd64daeeda6fa Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 19 Jan 2022 16:16:29 +0000 Subject: [PATCH 05/27] windows driver --- examples/animation.py | 2 +- src/textual/_windows_driver.py | 12 - src/textual/app.py | 4 +- src/textual/driver.py | 3 - src/textual/drivers/__init__.py | 0 .../linux_driver.py} | 20 +- src/textual/drivers/win32.py | 355 ++++++++++++++++++ src/textual/drivers/windows_driver.py | 148 ++++++++ 8 files changed, 517 insertions(+), 27 deletions(-) delete mode 100644 src/textual/_windows_driver.py create mode 100644 src/textual/drivers/__init__.py rename src/textual/{_linux_driver.py => drivers/linux_driver.py} (95%) create mode 100644 src/textual/drivers/win32.py create mode 100644 src/textual/drivers/windows_driver.py diff --git a/examples/animation.py b/examples/animation.py index 79edd4817..b6b58e1be 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -33,4 +33,4 @@ class SmoothApp(App): self.bar.layout_offset_x = -40 -SmoothApp.run(log="textual.log") +SmoothApp.run(log="textual.log", log_verbosity=3) diff --git a/src/textual/_windows_driver.py b/src/textual/_windows_driver.py deleted file mode 100644 index 8d7de90e6..000000000 --- a/src/textual/_windows_driver.py +++ /dev/null @@ -1,12 +0,0 @@ -from .driver import Driver - - -class WindowsDriver(Driver): - def start_application_mode(self) -> None: - pass - - def disable_input(self) -> None: - pass - - def stop_application_mode(self) -> None: - pass diff --git a/src/textual/app.py b/src/textual/app.py index f92e1bfc2..c664f9ff0 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -119,11 +119,11 @@ class App(MessagePump): Driver: A Driver class which manages input and display. """ if platform.system() == "Windows": - from ._windows_driver import WindowsDriver + from .drivers.windows_driver import WindowsDriver driver_class = WindowsDriver else: - from ._linux_driver import LinuxDriver + from .drivers.linux_driver import LinuxDriver driver_class = LinuxDriver return driver_class 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 95% rename from src/textual/_linux_driver.py rename to src/textual/drivers/linux_driver.py index a20e19f0e..057688988 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() @@ -215,11 +217,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..a73a05463 --- /dev/null +++ b/src/textual/drivers/win32.py @@ -0,0 +1,355 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 - 2021 Avram Lubkin, All Rights Reserved + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +Support functions and wrappers for calls to the Windows API +""" + +import atexit +import codecs +from collections import namedtuple +import ctypes +from ctypes import wintypes +import io +import msvcrt # pylint: disable=import-error +import os +import platform +import sys + +LPDWORD = ctypes.POINTER(wintypes.DWORD) +COORD = wintypes._COORD # pylint: disable=protected-access + +# 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 + +if tuple(int(num) for num in platform.version().split(".")) >= ( + 10, + 0, + 10586, +): + VTMODE_SUPPORTED = True + CBREAK_MODE = ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT + RAW_MODE = ENABLE_VIRTUAL_TERMINAL_INPUT +else: + VTMODE_SUPPORTED = False + CBREAK_MODE = ENABLE_PROCESSED_INPUT + RAW_MODE = 0 + +GTS_SUPPORTED = hasattr(os, "get_terminal_size") +TerminalSize = namedtuple("TerminalSize", ("columns", "lines")) + + +class ConsoleScreenBufferInfo( + ctypes.Structure +): # pylint: disable=too-few-public-methods + """ + Python representation of CONSOLE_SCREEN_BUFFER_INFO structure + https://docs.microsoft.com/en-us/windows/console/console-screen-buffer-info-str + """ + + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", wintypes.SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] + + +CSBIP = ctypes.POINTER(ConsoleScreenBufferInfo) + + +def _check_bool(result, func, args): # pylint: disable=unused-argument + """ + Used as an error handler for Windows calls + Gets last error if call is not successful + """ + + if not result: + raise ctypes.WinError(ctypes.get_last_error()) + return args + + +KERNEL32 = ctypes.WinDLL("kernel32", use_last_error=True) + +KERNEL32.GetConsoleCP.errcheck = _check_bool +KERNEL32.GetConsoleCP.argtypes = tuple() + +KERNEL32.GetConsoleMode.errcheck = _check_bool +KERNEL32.GetConsoleMode.argtypes = (wintypes.HANDLE, LPDWORD) + +KERNEL32.SetConsoleMode.errcheck = _check_bool +KERNEL32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD) + +KERNEL32.GetConsoleScreenBufferInfo.errcheck = _check_bool +KERNEL32.GetConsoleScreenBufferInfo.argtypes = (wintypes.HANDLE, CSBIP) + + +def get_csbi(filehandle=None): + """ + Args: + filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` + + Returns: + :py:class:`ConsoleScreenBufferInfo`: CONSOLE_SCREEN_BUFFER_INFO_ structure + + Wrapper for GetConsoleScreenBufferInfo_ + + If ``filehandle`` is :py:data:`None`, uses the filehandle of :py:data:`sys.__stdout__`. + + """ + + if filehandle is None: + filehandle = msvcrt.get_osfhandle(sys.__stdout__.fileno()) + + csbi = ConsoleScreenBufferInfo() + KERNEL32.GetConsoleScreenBufferInfo(filehandle, ctypes.byref(csbi)) + return csbi + + +def get_console_input_encoding(): + """ + Returns: + int: Current console mode + + Raises: + OSError: Error calling Windows API + + Query for the console input code page and provide an encoding + + If the code page can not be resolved to a Python encoding, :py:data:`None` is returned. + """ + + encoding = "cp%d" % KERNEL32.GetConsoleCP() + + try: + codecs.lookup(encoding) + except LookupError: + return None + + return encoding + + +def get_console_mode(filehandle): + """ + Args: + filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` + + Returns: + int: Current console mode + + Raises: + OSError: Error calling Windows API + + Wrapper for GetConsoleMode_ + """ + + mode = wintypes.DWORD() + KERNEL32.GetConsoleMode(filehandle, ctypes.byref(mode)) + return mode.value + + +def set_console_mode(filehandle, mode): + """ + Args: + filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` + mode(int): Desired console mode + + Raises: + OSError: Error calling Windows API + + Wrapper for SetConsoleMode_ + """ + + return bool(KERNEL32.SetConsoleMode(filehandle, mode)) + + +def setcbreak(filehandle): + """ + Args: + filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` + + Raises: + OSError: Error calling Windows API + + Convenience function which mimics :py:func:`tty.setcbreak` behavior + + All console input options are disabled except ``ENABLE_PROCESSED_INPUT`` + and, if supported, ``ENABLE_VIRTUAL_TERMINAL_INPUT`` + """ + + set_console_mode(filehandle, CBREAK_MODE) + + +def setraw(filehandle): + """ + Args: + filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` + + Raises: + OSError: Error calling Windows API + + Convenience function which mimics :py:func:`tty.setraw` behavior + + All console input options are disabled except, if supported, ``ENABLE_VIRTUAL_TERMINAL_INPUT`` + """ + + set_console_mode(filehandle, RAW_MODE) + + +def enable_vt_mode(filehandle=None): + """ + Args: + filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` + + Raises: + OSError: Error calling Windows API + + Enables virtual terminal processing mode for the given console + + If ``filehandle`` is :py:data:`None`, uses the filehandle of :py:data:`sys.__stdout__`. + """ + + if filehandle is None: + filehandle = msvcrt.get_osfhandle(sys.__stdout__.fileno()) + + mode = get_console_mode(filehandle) + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING + set_console_mode(filehandle, mode) + + +def get_terminal_size(fd): # pylint: disable=invalid-name + """ + Args: + fd(int): Python file descriptor + + Returns: + :py:class:`os.terminal_size`: Named tuple representing terminal size + + Convenience function for getting terminal size + + In Python 3.3 and above, this is a wrapper for :py:func:`os.get_terminal_size`. + In older versions of Python, this function calls GetConsoleScreenBufferInfo_. + """ + + # In Python 3.3+ we can let the standard library handle this + if GTS_SUPPORTED: + return os.get_terminal_size(fd) + + handle = msvcrt.get_osfhandle(fd) + window = get_csbi(handle).srWindow + return TerminalSize(window.Right - window.Left + 1, window.Bottom - window.Top + 1) + + +def flush_and_set_console(fd, mode): # pylint: disable=invalid-name + """ + Args: + filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` + mode(int): Desired console mode + + Attempts to set console to specified mode, but will not raise on failure + + If the file descriptor is STDOUT or STDERR, attempts to flush first + """ + + try: + if fd in (sys.__stdout__.fileno(), sys.__stderr__.fileno()): + sys.__stdout__.flush() + sys.__stderr__.flush() + except (AttributeError, TypeError, io.UnsupportedOperation): + pass + + try: + filehandle = msvcrt.get_osfhandle(fd) + set_console_mode(filehandle, mode) + except OSError: + pass + + +def get_term(fd, fallback=True): # pylint: disable=invalid-name + """ + Args: + fd(int): Python file descriptor + fallback(bool): Use fallback terminal type if type can not be determined + Returns: + str: Terminal type + + Attempts to determine and enable the current terminal type + + The current logic is: + + - If TERM is defined in the environment, the value is returned + - Else, if ANSICON is defined in the environment, ``'ansicon'`` is returned + - Else, if virtual terminal mode is natively supported, + it is enabled and ``'vtwin10'`` is returned + - Else, if ``fallback`` is ``True``, Ansicon is loaded, and ``'ansicon'`` is returned + - If no other conditions are satisfied, ``'unknown'`` is returned + + This logic may change in the future as additional terminal types are added. + """ + + # First try TERM + term = os.environ.get("TERM", None) + + if term is None: + + # See if ansicon is enabled + if os.environ.get("ANSICON", None): + term = "ansicon" + + # See if Windows Terminal is being used + elif os.environ.get("WT_SESSION", None): + term = "vtwin10" + + # See if the version of Windows supports VTMODE + elif VTMODE_SUPPORTED: + try: + filehandle = msvcrt.get_osfhandle(fd) + mode = get_console_mode(filehandle) + except OSError: + term = "unknown" + else: + atexit.register(flush_and_set_console, fd, mode) + # pylint: disable=unsupported-binary-operation + set_console_mode(filehandle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + term = "vtwin10" + + # Currently falling back to Ansicon for older versions of Windows + elif fallback: + import ansicon # pylint: disable=import-error,import-outside-toplevel + + ansicon.load() + + try: + filehandle = msvcrt.get_osfhandle(fd) + mode = get_console_mode(filehandle) + except OSError: + term = "unknown" + else: + atexit.register(flush_and_set_console, fd, mode) + set_console_mode(filehandle, mode ^ ENABLE_WRAP_AT_EOL_OUTPUT) + term = "ansicon" + + else: + term = "unknown" + + return term diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py new file mode 100644 index 000000000..8ace1caa9 --- /dev/null +++ b/src/textual/drivers/windows_driver.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +import asyncio +from codecs import getincrementaldecoder +import msvcrt +import os +import selectors +import signal +import sys +from threading import Event, Thread +from typing import TYPE_CHECKING + +from ..driver import Driver +from ..geometry import Size + +from . import win32 # +from .. import events +from .. import log +from .._types import MessageTarget +from .._xterm_parser import XTermParser + + +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._key_thread: Thread | None = None + + def _get_terminal_size(self) -> tuple[int, int]: + width, height = win32.get_terminal_size(self.out_fileno) + return (width, height) + + 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 + + # write("\x1b[?1007h") + self.console.file.flush() + + # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr + # extensions. + + 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() + + filehandle = msvcrt.get_osfhandle(self.out_fileno) + win32.enable_vt_mode(filehandle) + + self.console.set_alt_screen(True) + self._enable_mouse_support() + self.console.show_cursor(False) + self.console.file.write("\033[?1003h\n") + win32.setraw(msvcrt.get_osfhandle(self.in_fileno)) + + self._key_thread = Thread( + target=self.run_input_thread, args=(asyncio.get_event_loop(),) + ) + + width, height = win32.get_terminal_size(self.out_fileno) + + asyncio.run_coroutine_threadsafe( + self._target.post_message(events.Resize(self._target, Size(width, height))), + loop=loop, + ) + log("starting key thread") + self._key_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._key_thread is not None: + self._key_thread.join() + except Exception as error: + # TODO: log this + pass + + def stop_application_mode(self) -> None: + self.disable_input() + + with self.console: + self.console.set_alt_screen(False) + self.console.show_cursor(True) + + def run_input_thread(self, loop) -> None: + try: + self._run_input_thread(loop) + except Exception: + pass # TODO: log + + def _run_input_thread(self, loop) -> None: + log("input thread") + + selector = selectors.DefaultSelector() + selector.register(self.in_fileno, selectors.EVENT_READ) + + fileno = self.in_fileno + + def more_data() -> bool: + """Check if there is more data to parse.""" + for key, events in selector.select(0.01): + if events: + return True + return False + + parser = XTermParser(self._target, more_data) + + utf8_decoder = getincrementaldecoder("utf-8")().decode + decode = utf8_decoder + read = os.read + + log("starting thread") + try: + while not self.exit_event.is_set(): + selector_events = selector.select(0.1) + for _selector_key, mask in selector_events: + log(mask) + if mask | selectors.EVENT_READ: + unicode_data = decode(read(fileno, 1024)) + log("ket", unicode_data) + for event in parser.feed(unicode_data): + self.process_event(event) + except Exception as error: + log(error) + finally: + selector.close() From fc027a9f6c98b5b13d88a79bdfaffaa0635b85d4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 20 Jan 2022 13:36:53 +0000 Subject: [PATCH 06/27] Docstring for StyleProperty --- src/textual/css/_style_properties.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 3ed32ee8b..a319c7780 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -35,6 +35,10 @@ if TYPE_CHECKING: class ScalarProperty: + """ + Represents a numeric value and a unit. + """ + def __init__( self, units: set[Unit] | None = None, percent_unit: Unit = Unit.WIDTH ) -> None: @@ -52,9 +56,17 @@ class ScalarProperty: 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: + """ + Args: + obj (Styles): The Styles object. + value (float | Scalar | str | None): The value to set the 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. + """ if value is None: new_value = None elif isinstance(value, float): @@ -76,7 +88,6 @@ 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: From 4118d3eb4581344c5ff76b22b3c2eebd61c133f7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Jan 2022 16:02:56 +0000 Subject: [PATCH 07/27] win32 support --- src/textual/drivers/windows_driver.py | 100 ++++++++++++++++---------- 1 file changed, 64 insertions(+), 36 deletions(-) diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py index 8ace1caa9..f31ac5363 100644 --- a/src/textual/drivers/windows_driver.py +++ b/src/textual/drivers/windows_driver.py @@ -1,14 +1,17 @@ from __future__ import annotations import asyncio +from ctypes import windll +from ctypes.wintypes import BOOL, DWORD, HANDLE from codecs import getincrementaldecoder + import msvcrt import os import selectors import signal import sys from threading import Event, Thread -from typing import TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING from ..driver import Driver from ..geometry import Size @@ -22,6 +25,38 @@ from .._xterm_parser import XTermParser if TYPE_CHECKING: from rich.console import Console + from textual.app import App + +WAIT_TIMEOUT = 0x00000102 + + +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 = windll.kernel32.WaitForMultipleObjects( + len(handle_array), handle_array, BOOL(False), DWORD(timeout) + ) + + if ret == WAIT_TIMEOUT: + return None + else: + return handles[ret] class WindowsDriver(Driver): @@ -64,17 +99,20 @@ class WindowsDriver(Driver): loop = asyncio.get_event_loop() - filehandle = msvcrt.get_osfhandle(self.out_fileno) - win32.enable_vt_mode(filehandle) + win32.enable_vt_mode(msvcrt.get_osfhandle(self.out_fileno)) + win32.setraw(msvcrt.get_osfhandle(self.in_fileno)) self.console.set_alt_screen(True) self._enable_mouse_support() self.console.show_cursor(False) self.console.file.write("\033[?1003h\n") - win32.setraw(msvcrt.get_osfhandle(self.in_fileno)) + + from .._context import active_app + + app = active_app.get() self._key_thread = Thread( - target=self.run_input_thread, args=(asyncio.get_event_loop(),) + target=self.run_input_thread, args=(asyncio.get_event_loop(), app) ) width, height = win32.get_terminal_size(self.out_fileno) @@ -83,7 +121,9 @@ class WindowsDriver(Driver): self._target.post_message(events.Resize(self._target, Size(width, height))), loop=loop, ) - log("starting key thread") + + from .._context import active_app + self._key_thread.start() def disable_input(self) -> None: @@ -104,45 +144,33 @@ class WindowsDriver(Driver): self.console.set_alt_screen(False) self.console.show_cursor(True) - def run_input_thread(self, loop) -> None: + def run_input_thread(self, loop, app: App) -> None: try: - self._run_input_thread(loop) - except Exception: - pass # TODO: log + self._run_input_thread(loop, app) + except Exception as error: + app.log(error) - def _run_input_thread(self, loop) -> None: - log("input thread") + def _run_input_thread(self, loop, app: App) -> None: + app.log("input thread") - selector = selectors.DefaultSelector() - selector.register(self.in_fileno, selectors.EVENT_READ) - - fileno = self.in_fileno - - def more_data() -> bool: - """Check if there is more data to parse.""" - for key, events in selector.select(0.01): - if events: - return True - return False - - parser = XTermParser(self._target, more_data) + parser = XTermParser(self._target, lambda: False) utf8_decoder = getincrementaldecoder("utf-8")().decode decode = utf8_decoder read = os.read - log("starting thread") + input_handle = msvcrt.get_osfhandle(self.in_fileno) + app.log("input_handle", input_handle) + app.log("starting thread") try: while not self.exit_event.is_set(): - selector_events = selector.select(0.1) - for _selector_key, mask in selector_events: - log(mask) - if mask | selectors.EVENT_READ: - unicode_data = decode(read(fileno, 1024)) - log("ket", unicode_data) - for event in parser.feed(unicode_data): - self.process_event(event) + if wait_for_handles([input_handle], 100) is None: + continue + unicode_data = decode(read(self.in_fileno, 1024)) + app.log("key", repr(unicode_data)) + for event in parser.feed(unicode_data): + self.process_event(event) except Exception as error: - log(error) + app.log(error) finally: - selector.close() + app.log("input thread finished") From 683d5b8a57b95fce2d3eda9d527c74bee7ed4ee5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 20 Jan 2022 16:20:00 +0000 Subject: [PATCH 08/27] Adding additional documentation for style properties --- src/textual/_box.py | 4 ++ src/textual/css/_style_properties.py | 74 ++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 15 deletions(-) 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/css/_style_properties.py b/src/textual/css/_style_properties.py index a319c7780..3c93cee19 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -32,12 +32,11 @@ from ._error_tools import friendly_list if TYPE_CHECKING: from .styles import Styles from .styles import DockGroup + from .._box import BoxType class ScalarProperty: - """ - Represents a numeric value and a unit. - """ + """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 @@ -53,6 +52,14 @@ class ScalarProperty: def __get__( self, obj: Styles, objtype: type[Styles] | None = None ) -> Scalar | None: + """ + 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 @@ -60,7 +67,7 @@ class ScalarProperty: """ Args: obj (Styles): The Styles object. - value (float | Scalar | str | None): The value to set the property to. + 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, @@ -91,6 +98,9 @@ class ScalarProperty: class BoxProperty: + """Descriptor for getting and setting outlines and borders along a single edge. + For example "border-right", "outline-bottom", etc. + """ DEFAULT = ("", Style()) @@ -102,13 +112,27 @@ class BoxProperty: def __get__( self, obj: Styles, objtype: type[Styles] | None = None - ) -> tuple[str, Style]: + ) -> tuple[BoxType, Style]: + """ + 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): + """ + 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. + """ if border is None: new_value = None else: @@ -121,17 +145,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 @@ -160,6 +183,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", @@ -169,6 +194,14 @@ class BorderProperty: ) def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Edges: + """ + 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), @@ -181,10 +214,21 @@ 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: + """ + 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. + """ top, right, bottom, left = self._properties obj.refresh() if border is None: From 2f84bd94c28ada0e7194b546fcd932f534a2046a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jan 2022 15:22:00 +0000 Subject: [PATCH 09/27] Some more docs on style properties --- src/textual/css/_style_properties.py | 72 ++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 3c93cee19..90a89e9a5 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -52,7 +52,8 @@ 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 @@ -64,7 +65,8 @@ class ScalarProperty: return value 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. @@ -113,7 +115,8 @@ class BoxProperty: def __get__( self, obj: Styles, objtype: type[Styles] | None = None ) -> tuple[BoxType, Style]: - """ + """Get the box property + Args: obj (Styles): The Styles object objtype (type[Styles]): The Styles class @@ -126,7 +129,8 @@ class BoxProperty: return value or self.DEFAULT 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, @@ -194,7 +198,8 @@ 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 @@ -218,7 +223,8 @@ class BorderProperty: | 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): @@ -267,17 +273,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) @@ -286,7 +300,14 @@ 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"``). + """ obj.refresh() if style is None: setattr(obj, self._color_name, None) @@ -301,38 +322,59 @@ 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. + """ 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"``). + """ obj.refresh(True) spacing = Spacing.unpack(spacing) setattr(obj, self._internal_name, spacing) - return spacing class DocksProperty: + """Descriptor for getting and setting the docks property.""" + def __get__( self, obj: Styles, objtype: type[Styles] | None = None ) -> tuple[DockGroup, ...]: 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(True) if docks is None: obj._rule_docks = None else: obj._rule_docks = tuple(docks) - return docks class DockProperty: From 3434ffd944a78ec16a18b0a199d0ec71df5728ee Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jan 2022 11:33:55 +0000 Subject: [PATCH 10/27] Document a few more style properties --- src/textual/css/_style_properties.py | 65 +++++++++++++++++++++++++--- src/textual/css/styles.py | 6 --- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 90a89e9a5..0540bcff8 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -356,11 +356,22 @@ class SpacingProperty: class DocksProperty: - """Descriptor for getting and setting the docks property.""" + """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): @@ -378,27 +389,68 @@ class DocksProperty: 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(True) obj._rule_dock = spacing - return spacing 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 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``. + """ obj.refresh(True) if isinstance(offset, ScalarOffset): setattr(obj, self._internal_name, offset) @@ -416,7 +468,6 @@ class OffsetProperty: ) _offset = ScalarOffset(scalar_x, scalar_y) setattr(obj, self._internal_name, _offset) - return offset class IntegerProperty: diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 533167347..296db9b7c 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -46,12 +46,6 @@ from ._style_properties import ( from .types import Display, Edge, Visibility -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - - if TYPE_CHECKING: from ..dom import DOMNode From 764f14b82668580ba8f29bac06a3ae460be6621c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jan 2022 13:27:57 +0000 Subject: [PATCH 11/27] Rename StringProperty, document some more properties --- src/textual/css/_style_properties.py | 81 ++++++++++++++++++++++++---- src/textual/css/styles.py | 8 +-- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 0540bcff8..213115ac1 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -447,9 +447,14 @@ class OffsetProperty: Args: obj: The Styles class - offset: A ScalarOffset object, or a tuple of the form ``(x, y)`` indicating + 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``. + 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(True) if isinstance(offset, ScalarOffset): @@ -471,22 +476,45 @@ class OffsetProperty: 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 @@ -496,9 +524,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: @@ -506,23 +552,38 @@ 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 + """ obj.refresh(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: diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 296db9b7c..5c51dc1c3 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -38,7 +38,7 @@ from ._style_properties import ( NameListProperty, ScalarProperty, SpacingProperty, - StringProperty, + StringEnumProperty, StyleProperty, StyleFlagsProperty, TransitionsProperty, @@ -102,9 +102,9 @@ class Styles: important: set[str] = field(default_factory=set) - display = StringProperty(VALID_DISPLAY, "block") - visibility = StringProperty(VALID_VISIBILITY, "visible") - layout = StringProperty(VALID_LAYOUT, "dock") + display = StringEnumProperty(VALID_DISPLAY, "block") + visibility = StringEnumProperty(VALID_VISIBILITY, "visible") + layout = StringEnumProperty(VALID_LAYOUT, "dock") text = StyleProperty() text_color = ColorProperty() From f549ae3f7c0312b209c3e40956c42fa46a9a6386 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jan 2022 14:00:49 +0000 Subject: [PATCH 12/27] More docs, ensuring error cases are covered --- src/textual/css/_style_properties.py | 164 ++++++++++++++++++++------- 1 file changed, 120 insertions(+), 44 deletions(-) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 213115ac1..6e229172c 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 @@ -55,8 +55,8 @@ class ScalarProperty: """Get the scalar property Args: - obj (Styles): The Styles object - objtype (type[Styles]): The Styles class + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class Returns: The Scalar object or ``None`` if it's not set. @@ -68,13 +68,17 @@ class ScalarProperty: """Set the scalar property Args: - obj (Styles): The Styles object. + 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 @@ -118,8 +122,8 @@ class BoxProperty: """Get the box property Args: - obj (Styles): The Styles object - objtype (type[Styles]): The Styles class + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class Returns: A ``tuple[BoxType, Style]`` containing the string type of the box and @@ -132,10 +136,13 @@ class BoxProperty: """Set the box property Args: - obj (Styles): The Styles object. + 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 @@ -201,8 +208,8 @@ class BorderProperty: """Get the border Args: - obj (Styles): The Styles object - objtype (type[Styles]): The Styles class + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class Returns: An ``Edges`` object describing the type and style of each edge. @@ -226,14 +233,17 @@ class BorderProperty: """Set the border Args: - obj (Styles): The Styles object. + 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 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() @@ -286,8 +296,8 @@ class StyleProperty: """Get the Style Args: - obj (Styles): The Styles object - objtype (type[Styles]): The Styles class + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class Returns: A ``Style`` object. @@ -304,9 +314,12 @@ class StyleProperty: """Set the Style Args: - obj (Styles): The Styles object. + 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: @@ -334,11 +347,11 @@ class SpacingProperty: """Get the Spacing Args: - obj (Styles): The Styles object - objtype (type[Styles]): The Styles class + obj (Styles): The ``Styles`` object + objtype (type[Styles]): The ``Styles`` class Returns: - Spacing: The Spacing. + Spacing: The Spacing. If unset, returns the null spacing ``(0, 0, 0, 0)``. """ return getattr(obj, self._internal_name) or NULL_SPACING @@ -346,9 +359,13 @@ class SpacingProperty: """Set the Spacing Args: - obj (Styles): The Styles object. + 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(True) spacing = Spacing.unpack(spacing) @@ -366,11 +383,11 @@ class DocksProperty: """Get the Docks property Args: - obj (Styles): The Styles object. - objtype (type[Styles]): The Styles class. + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. Returns: - tuple[DockGroup, ...]: A tuple containing the defined docks. + tuple[DockGroup, ...]: A ``tuple`` containing the defined docks. """ return obj._rule_docks or () @@ -378,7 +395,7 @@ class DocksProperty: """Set the Docks property Args: - obj (Styles): The Styles object. + obj (Styles): The ``Styles`` object. docks (Iterable[DockGroup]): Iterable of DockGroups """ obj.refresh(True) @@ -399,8 +416,8 @@ class DockProperty: """Get the Dock property Args: - obj (Styles): The Styles object. - objtype (type[Styles]): The Styles class. + 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. @@ -411,7 +428,7 @@ class DockProperty: """Set the Dock property Args: - obj (Styles): The Styles object + obj (Styles): The ``Styles`` object spacing (str | None): The spacing to use. """ obj.refresh(True) @@ -431,11 +448,11 @@ class OffsetProperty: """Get the offset Args: - obj (Styles): The Styles object. - objtype (type[Styles]): The Styles class. + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. Returns: - ScalarOffset: The ScalarOffset indicating the adjustment that + 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( @@ -446,9 +463,9 @@ class OffsetProperty: """Set the offset Args: - obj: The Styles class + 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 + 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")``. @@ -486,8 +503,8 @@ class IntegerProperty: """Get the integer property, or the default ``0`` if not set. Args: - obj (Styles): The Styles object. - objtype (type[Styles]): The Styles class. + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. Returns: int: The integer property value @@ -498,7 +515,7 @@ class IntegerProperty: """Set the integer property Args: - obj: The Styles object + obj: The ``Styles`` object value: The value to set the integer to Raises: @@ -527,8 +544,8 @@ class StringEnumProperty: """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. + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. Returns: str: The string property value @@ -539,7 +556,7 @@ class StringEnumProperty: """Set the string property and ensure it is in the set of allowed values. Args: - obj (Styles): The Styles object + obj (Styles): The ``Styles`` object value (str, optional): The string value to set the property to. Raises: @@ -565,8 +582,8 @@ class NameProperty: """Get the name property Args: - obj (Styles): The Styles object. - objtype (type[Styles]): The Styles class. + obj (Styles): The ``Styles`` object. + objtype (type[Styles]): The ``Styles`` class. Returns: str: The name @@ -577,8 +594,11 @@ class NameProperty: """Set the name property Args: - obj: The Styles object + obj: The ``Styles`` object name: The name to set the property to + + Raises: + StyleTypeError: If the value is not a ``str``. """ obj.refresh(True) if not isinstance(name, str): @@ -612,14 +632,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) @@ -629,10 +671,10 @@ 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", @@ -652,9 +694,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) @@ -663,13 +724,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}" @@ -677,4 +742,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 {} From c03dd3689a83f8825e83e9223b8862c7573ac55b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 27 Jan 2022 12:43:30 +0000 Subject: [PATCH 13/27] Fixing merge conflicts --- src/textual/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/view.py b/src/textual/view.py index f6078969f..e21466947 100644 --- a/src/textual/view.py +++ b/src/textual/view.py @@ -105,7 +105,7 @@ class View(Widget): placements = [ placement.apply_margin() - for placement in self._layout.arrange(self, size, scroll) + for placement in self.layout.arrange(self, size, scroll) ] self._cached_arrangement = (size, scroll, placements) From 988838a872d2c7af6a1113546ace4f15b74a3254 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 27 Jan 2022 16:54:10 +0000 Subject: [PATCH 14/27] working windows driver --- examples/animation.py | 4 +- src/textual/drivers/linux_driver.py | 22 +- src/textual/drivers/win32.py | 483 +++++++++++--------------- src/textual/drivers/windows_driver.py | 54 ++- 4 files changed, 254 insertions(+), 309 deletions(-) diff --git a/examples/animation.py b/examples/animation.py index b6b58e1be..2578834c2 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", log_verbosity=3) + +SmoothApp.run(log="textual.log", log_verbosity=2) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 057688988..45db1925a 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -157,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: diff --git a/src/textual/drivers/win32.py b/src/textual/drivers/win32.py index a73a05463..3455b31bb 100644 --- a/src/textual/drivers/win32.py +++ b/src/textual/drivers/win32.py @@ -1,27 +1,23 @@ -# -*- coding: utf-8 -*- -# Copyright 2019 - 2021 Avram Lubkin, All Rights Reserved +from asyncio import AbstractEventLoop, run_coroutine_threadsafe +from codecs import getincrementaldecoder -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -""" -Support functions and wrappers for calls to the Windows API -""" - -import atexit -import codecs -from collections import namedtuple import ctypes -from ctypes import wintypes -import io -import msvcrt # pylint: disable=import-error +from ctypes import byref, Structure, Union, wintypes +from ctypes.wintypes import CHAR, HANDLE, WCHAR, BOOL, WORD, DWORD, SHORT, UINT +import msvcrt import os -import platform import sys +import threading -LPDWORD = ctypes.POINTER(wintypes.DWORD) -COORD = wintypes._COORD # pylint: disable=protected-access +from tkinter.tix import WINDOW +from typing import IO, Callable, List, Optional + +from ..geometry import Size +from ..events import Event, Key, Resize +from .._types import EventTarget +from .._xterm_parser import XTermParser + +KERNEL32 = ctypes.WinDLL("kernel32", use_last_error=True) # Console input modes ENABLE_ECHO_INPUT = 0x0004 @@ -41,315 +37,248 @@ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 DISABLE_NEWLINE_AUTO_RETURN = 0x0008 ENABLE_LVB_GRID_WORLDWIDE = 0x0010 -if tuple(int(num) for num in platform.version().split(".")) >= ( - 10, - 0, - 10586, -): - VTMODE_SUPPORTED = True - CBREAK_MODE = ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT - RAW_MODE = ENABLE_VIRTUAL_TERMINAL_INPUT -else: - VTMODE_SUPPORTED = False - CBREAK_MODE = ENABLE_PROCESSED_INPUT - RAW_MODE = 0 +STD_INPUT_HANDLE = -10 +STD_OUTPUT_HANDLE = -11 -GTS_SUPPORTED = hasattr(os, "get_terminal_size") -TerminalSize = namedtuple("TerminalSize", ("columns", "lines")) +WAIT_TIMEOUT = 0x00000102 + +GetStdHandle = KERNEL32.GetStdHandle +GetStdHandle.argtypes = [wintypes.DWORD] +GetStdHandle.restype = wintypes.HANDLE -class ConsoleScreenBufferInfo( - ctypes.Structure -): # pylint: disable=too-few-public-methods - """ - Python representation of CONSOLE_SCREEN_BUFFER_INFO structure - https://docs.microsoft.com/en-us/windows/console/console-screen-buffer-info-str - """ +class COORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/coord-str""" _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", wintypes.WORD), - ("srWindow", wintypes.SMALL_RECT), - ("dwMaximumWindowSize", COORD), + ("X", SHORT), + ("Y", SHORT), ] -CSBIP = ctypes.POINTER(ConsoleScreenBufferInfo) +class uChar(Union): + """https://docs.microsoft.com/en-us/windows/console/key-event-record-str""" + + _fields_ = [ + ("AsciiChar", CHAR), + ("UnicodeChar", WCHAR), + ] -def _check_bool(result, func, args): # pylint: disable=unused-argument - """ - Used as an error handler for Windows calls - Gets last error if call is not successful - """ +class KEY_EVENT_RECORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/key-event-record-str""" - if not result: - raise ctypes.WinError(ctypes.get_last_error()) - return args + _fields_ = [ + ("bKeyDown", BOOL), + ("wRepeatCount", WORD), + ("wVirtualKeyCode", WORD), + ("wVirtualScanCode", WORD), + ("uChar", uChar), + ("dwControlKeyState", DWORD), + ] -KERNEL32 = ctypes.WinDLL("kernel32", use_last_error=True) +class MOUSE_EVENT_RECORD(Structure): + """https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str""" -KERNEL32.GetConsoleCP.errcheck = _check_bool -KERNEL32.GetConsoleCP.argtypes = tuple() - -KERNEL32.GetConsoleMode.errcheck = _check_bool -KERNEL32.GetConsoleMode.argtypes = (wintypes.HANDLE, LPDWORD) - -KERNEL32.SetConsoleMode.errcheck = _check_bool -KERNEL32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD) - -KERNEL32.GetConsoleScreenBufferInfo.errcheck = _check_bool -KERNEL32.GetConsoleScreenBufferInfo.argtypes = (wintypes.HANDLE, CSBIP) + _fields_ = [ + ("dwMousePosition", COORD), + ("dwButtonState", DWORD), + ("dwControlKeyState", DWORD), + ("dwEventFlags", DWORD), + ] -def get_csbi(filehandle=None): - """ +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: - filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` + file (IO): A file like object. + mode (int): New mode. Returns: - :py:class:`ConsoleScreenBufferInfo`: CONSOLE_SCREEN_BUFFER_INFO_ structure - - Wrapper for GetConsoleScreenBufferInfo_ - - If ``filehandle`` is :py:data:`None`, uses the filehandle of :py:data:`sys.__stdout__`. - + bool: True on success, otherwise False. """ - - if filehandle is None: - filehandle = msvcrt.get_osfhandle(sys.__stdout__.fileno()) - - csbi = ConsoleScreenBufferInfo() - KERNEL32.GetConsoleScreenBufferInfo(filehandle, ctypes.byref(csbi)) - return csbi + windows_filehandle = msvcrt.get_osfhandle(file.fileno()) + success = KERNEL32.SetConsoleMode(windows_filehandle, mode) + return success -def get_console_input_encoding(): - """ - Returns: - int: Current console mode +def _get_console_mode(file: IO) -> int: + """Get the console mode for a given file (stdout or stdin) - Raises: - OSError: Error calling Windows API - - Query for the console input code page and provide an encoding - - If the code page can not be resolved to a Python encoding, :py:data:`None` is returned. - """ - - encoding = "cp%d" % KERNEL32.GetConsoleCP() - - try: - codecs.lookup(encoding) - except LookupError: - return None - - return encoding - - -def get_console_mode(filehandle): - """ Args: - filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` + file (IO): A file-like object. Returns: - int: Current console mode - - Raises: - OSError: Error calling Windows API - - Wrapper for GetConsoleMode_ + int: The current console mode. """ - + windows_filehandle = msvcrt.get_osfhandle(file.fileno()) mode = wintypes.DWORD() - KERNEL32.GetConsoleMode(filehandle, ctypes.byref(mode)) + KERNEL32.GetConsoleMode(windows_filehandle, ctypes.byref(mode)) return mode.value -def set_console_mode(filehandle, mode): - """ - Args: - filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` - mode(int): Desired console mode - - Raises: - OSError: Error calling Windows API - - Wrapper for SetConsoleMode_ - """ - - return bool(KERNEL32.SetConsoleMode(filehandle, mode)) - - -def setcbreak(filehandle): - """ - Args: - filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` - - Raises: - OSError: Error calling Windows API - - Convenience function which mimics :py:func:`tty.setcbreak` behavior - - All console input options are disabled except ``ENABLE_PROCESSED_INPUT`` - and, if supported, ``ENABLE_VIRTUAL_TERMINAL_INPUT`` - """ - - set_console_mode(filehandle, CBREAK_MODE) - - -def setraw(filehandle): - """ - Args: - filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` - - Raises: - OSError: Error calling Windows API - - Convenience function which mimics :py:func:`tty.setraw` behavior - - All console input options are disabled except, if supported, ``ENABLE_VIRTUAL_TERMINAL_INPUT`` - """ - - set_console_mode(filehandle, RAW_MODE) - - -def enable_vt_mode(filehandle=None): - """ - Args: - filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` - - Raises: - OSError: Error calling Windows API - - Enables virtual terminal processing mode for the given console - - If ``filehandle`` is :py:data:`None`, uses the filehandle of :py:data:`sys.__stdout__`. - """ - - if filehandle is None: - filehandle = msvcrt.get_osfhandle(sys.__stdout__.fileno()) - - mode = get_console_mode(filehandle) - mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING - set_console_mode(filehandle, mode) - - -def get_terminal_size(fd): # pylint: disable=invalid-name - """ - Args: - fd(int): Python file descriptor +def enable_application_mode() -> Callable[[], None]: + """Enable application mode. Returns: - :py:class:`os.terminal_size`: Named tuple representing terminal size - - Convenience function for getting terminal size - - In Python 3.3 and above, this is a wrapper for :py:func:`os.get_terminal_size`. - In older versions of Python, this function calls GetConsoleScreenBufferInfo_. + Callable[[], None]: A callable that will restore terminal to previous state. """ - # In Python 3.3+ we can let the standard library handle this - if GTS_SUPPORTED: - return os.get_terminal_size(fd) + terminal_in = sys.stdin + terminal_out = sys.stdout - handle = msvcrt.get_osfhandle(fd) - window = get_csbi(handle).srWindow - return TerminalSize(window.Right - window.Left + 1, window.Bottom - window.Top + 1) + 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 flush_and_set_console(fd, mode): # pylint: disable=invalid-name +def _wait_for_handles(handles: List[HANDLE], timeout: int = -1) -> Optional[HANDLE]: """ - Args: - filehandle(int): Windows filehandle object as returned by :py:func:`msvcrt.get_osfhandle` - mode(int): Desired console mode - - Attempts to set console to specified mode, but will not raise on failure - - If the file descriptor is STDOUT or STDERR, attempts to flush first + 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) - try: - if fd in (sys.__stdout__.fileno(), sys.__stderr__.fileno()): - sys.__stdout__.flush() - sys.__stderr__.flush() - except (AttributeError, TypeError, io.UnsupportedOperation): - pass + ret: int = KERNEL32.WaitForMultipleObjects( + len(handle_array), handle_array, BOOL(False), DWORD(timeout) + ) - try: - filehandle = msvcrt.get_osfhandle(fd) - set_console_mode(filehandle, mode) - except OSError: - pass + if ret == WAIT_TIMEOUT: + return None + else: + return handles[ret] -def get_term(fd, fallback=True): # pylint: disable=invalid-name - """ - Args: - fd(int): Python file descriptor - fallback(bool): Use fallback terminal type if type can not be determined - Returns: - str: Terminal type +class EventMonitor(threading.Thread): + """A thread to send key / window events to Textual loop.""" - Attempts to determine and enable the current terminal type + 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__() - The current logic is: + def run(self) -> None: + self.app.log("event monitor thread started") + exit_requested = self.exit_event.is_set + parser = XTermParser(self.target, lambda: False) - - If TERM is defined in the environment, the value is returned - - Else, if ANSICON is defined in the environment, ``'ansicon'`` is returned - - Else, if virtual terminal mode is natively supported, - it is enabled and ``'vtwin10'`` is returned - - Else, if ``fallback`` is ``True``, Ansicon is loaded, and ``'ansicon'`` is returned - - If no other conditions are satisfied, ``'unknown'`` is returned + try: + read_count = wintypes.DWORD(0) + hIn = GetStdHandle(STD_INPUT_HANDLE) - This logic may change in the future as additional terminal types are added. - """ + MAX_EVENTS = 1024 + KEY_EVENT = 0x0001 + WINDOW_BUFFER_SIZE_EVENT = 0x0004 - # First try TERM - term = os.environ.get("TERM", None) + arrtype = INPUT_RECORD * MAX_EVENTS + input_records = arrtype() + ReadConsoleInputW = KERNEL32.ReadConsoleInputW + keys: List[str] = [] - if term is None: + while not exit_requested(): + if _wait_for_handles([hIn], 100) is None: + continue + del keys[:] + ReadConsoleInputW( + hIn, byref(input_records), MAX_EVENTS, byref(read_count) + ) + read_input_records = input_records[: read_count.value] - # See if ansicon is enabled - if os.environ.get("ANSICON", None): - term = "ansicon" + apppend_key = keys.append + 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 = input_record.Event.KeyEvent + key = key_event.uChar.UnicodeChar + if key_event.bKeyDown or key == "\x1b": + apppend_key(key) + elif event_type == WINDOW_BUFFER_SIZE_EVENT: + size = input_record.Event.WindowBufferSizeEvent.dwSize + new_size = (size.X, size.Y) - # See if Windows Terminal is being used - elif os.environ.get("WT_SESSION", None): - term = "vtwin10" + if keys: + for event in parser.feed("".join(keys)): + self.process_event(event) + if new_size is not None: + self.on_size_change(*new_size) - # See if the version of Windows supports VTMODE - elif VTMODE_SUPPORTED: - try: - filehandle = msvcrt.get_osfhandle(fd) - mode = get_console_mode(filehandle) - except OSError: - term = "unknown" - else: - atexit.register(flush_and_set_console, fd, mode) - # pylint: disable=unsupported-binary-operation - set_console_mode(filehandle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) - term = "vtwin10" + except Exception as error: + self.app.log("EVENT MONITOR ERROR", error) + self.app.log("event monitor thread finished") - # Currently falling back to Ansicon for older versions of Windows - elif fallback: - import ansicon # pylint: disable=import-error,import-outside-toplevel - - ansicon.load() - - try: - filehandle = msvcrt.get_osfhandle(fd) - mode = get_console_mode(filehandle) - except OSError: - term = "unknown" - else: - atexit.register(flush_and_set_console, fd, mode) - set_console_mode(filehandle, mode ^ ENABLE_WRAP_AT_EOL_OUTPUT) - term = "ansicon" - - else: - term = "unknown" - - return term + 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 index f31ac5363..743d35b11 100644 --- a/src/textual/drivers/windows_driver.py +++ b/src/textual/drivers/windows_driver.py @@ -7,11 +7,9 @@ from codecs import getincrementaldecoder import msvcrt import os -import selectors -import signal import sys from threading import Event, Thread -from typing import List, Optional, TYPE_CHECKING +from typing import Callable, List, Optional, TYPE_CHECKING from ..driver import Driver from ..geometry import Size @@ -69,10 +67,8 @@ class WindowsDriver(Driver): self.exit_event = Event() self._key_thread: Thread | None = None - - def _get_terminal_size(self) -> tuple[int, int]: - width, height = win32.get_terminal_size(self.out_fileno) - return (width, height) + self._event_thread: Thread | None = None + self._restore_console: Callable[[], None] | None = None def _enable_mouse_support(self) -> None: write = self.console.file.write @@ -99,8 +95,7 @@ class WindowsDriver(Driver): loop = asyncio.get_event_loop() - win32.enable_vt_mode(msvcrt.get_osfhandle(self.out_fileno)) - win32.setraw(msvcrt.get_osfhandle(self.in_fileno)) + self._restore_console = win32.enable_application_mode() self.console.set_alt_screen(True) self._enable_mouse_support() @@ -111,11 +106,13 @@ class WindowsDriver(Driver): app = active_app.get() - self._key_thread = Thread( - target=self.run_input_thread, args=(asyncio.get_event_loop(), app) + # self._key_thread = Thread( + # target=self.run_input_thread, args=(asyncio.get_event_loop(), app) + # ) + self._event_thread = win32.EventMonitor( + loop, app, self._target, self.exit_event, self.process_event ) - - width, height = win32.get_terminal_size(self.out_fileno) + width, height = os.get_terminal_size(self.out_fileno) asyncio.run_coroutine_threadsafe( self._target.post_message(events.Resize(self._target, Size(width, height))), @@ -124,22 +121,26 @@ class WindowsDriver(Driver): from .._context import active_app - self._key_thread.start() + # self._key_thread.start() + + 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._key_thread is not None: - self._key_thread.join() + 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) @@ -162,12 +163,29 @@ class WindowsDriver(Driver): input_handle = msvcrt.get_osfhandle(self.in_fileno) app.log("input_handle", input_handle) app.log("starting thread") + import time + + terminal_size = os.get_terminal_size(self.out_fileno) + import shutil + try: while not self.exit_event.is_set(): + + new_terminal_size = os.get_terminal_size(self.out_fileno) + + if new_terminal_size != terminal_size: + app.log("SIZE CHANGE", new_terminal_size) + terminal_size = new_terminal_size + width, height = new_terminal_size + event = events.Resize(self._target, Size(width, height)) + app.log(event) + self.console.size = (width, height) + self.send_event(event) + if wait_for_handles([input_handle], 100) is None: continue unicode_data = decode(read(self.in_fileno, 1024)) - app.log("key", repr(unicode_data)) + # app.log("key", repr(unicode_data)) for event in parser.feed(unicode_data): self.process_event(event) except Exception as error: From b4358f887b8ddb9592693d61f07bd7bdd4a7789a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Jan 2022 10:13:30 +0000 Subject: [PATCH 15/27] re-construct console --- CHANGELOG.md | 6 ++++++ examples/animation.py | 2 +- src/textual/app.py | 10 +++++++--- src/textual/drivers/win32.py | 4 +--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 575952148..8feba707c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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] - Unreleased + +### Added + +- Added Windows Driver + ## [1.1.14] - 2022-01-09 ### Changed diff --git a/examples/animation.py b/examples/animation.py index 2578834c2..bf5f9ae8e 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -32,7 +32,7 @@ class SmoothApp(App): self.bar.layout_offset_x = -40 - self.set_timer(10, lambda: self.action("quit")) + # self.set_timer(10, lambda: self.action("quit")) SmoothApp.run(log="textual.log", log_verbosity=2) diff --git a/src/textual/app.py b/src/textual/app.py index c664f9ff0..4c6f58873 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2,7 +2,6 @@ from __future__ import annotations import os import asyncio -from functools import partial import platform from typing import Any, Callable, ClassVar, Type, TypeVar import warnings @@ -31,6 +30,8 @@ from .view import View from .views import DockView from .widget import Widget, Reactive +PLATFORM = platform.system() +WINDOWS = PLATFORM == "Windows" # asyncio will warn against resources not being cleared warnings.simplefilter("always", ResourceWarning) @@ -118,7 +119,7 @@ class App(MessagePump): Returns: Driver: A Driver class which manages input and display. """ - if platform.system() == "Windows": + if WINDOWS: from .drivers.windows_driver import WindowsDriver driver_class = WindowsDriver @@ -302,6 +303,7 @@ class App(MessagePump): self.console.print_exception() else: try: + self.console = Console() self.title = self._title self.refresh() await self.animator.start() @@ -344,7 +346,9 @@ class App(MessagePump): await self.close_messages() def refresh(self, repaint: bool = True, layout: bool = False) -> 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/drivers/win32.py b/src/textual/drivers/win32.py index 3455b31bb..55ca6cfe2 100644 --- a/src/textual/drivers/win32.py +++ b/src/textual/drivers/win32.py @@ -1,5 +1,4 @@ from asyncio import AbstractEventLoop, run_coroutine_threadsafe -from codecs import getincrementaldecoder import ctypes from ctypes import byref, Structure, Union, wintypes @@ -9,11 +8,10 @@ import os import sys import threading -from tkinter.tix import WINDOW from typing import IO, Callable, List, Optional from ..geometry import Size -from ..events import Event, Key, Resize +from ..events import Event, Resize from .._types import EventTarget from .._xterm_parser import XTermParser From ffcbe4ba1e04f0bace7d7e8b87aa2e0390555b00 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Jan 2022 11:21:14 +0000 Subject: [PATCH 16/27] discard console param --- src/textual/app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 4c6f58873..0000f068c 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -61,7 +61,6 @@ class App(MessagePump): def __init__( self, - console: Console | None = None, screen: bool = True, driver_class: Type[Driver] | None = None, log: str = "", @@ -76,7 +75,7 @@ class App(MessagePump): 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 self.get_driver_class() From 32a3bea47fc15e1f0f1a7e1560abecd150fbe016 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Jan 2022 11:29:55 +0000 Subject: [PATCH 17/27] tidy and comment --- src/textual/drivers/win32.py | 31 ++++--- src/textual/drivers/windows_driver.py | 125 ++------------------------ 2 files changed, 25 insertions(+), 131 deletions(-) diff --git a/src/textual/drivers/win32.py b/src/textual/drivers/win32.py index 55ca6cfe2..66c31540f 100644 --- a/src/textual/drivers/win32.py +++ b/src/textual/drivers/win32.py @@ -1,19 +1,16 @@ -from asyncio import AbstractEventLoop, run_coroutine_threadsafe - import ctypes -from ctypes import byref, Structure, Union, wintypes -from ctypes.wintypes import CHAR, HANDLE, WCHAR, BOOL, WORD, DWORD, SHORT, UINT import msvcrt -import os 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 ..geometry import Size -from ..events import Event, Resize 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) @@ -243,33 +240,43 @@ class EventMonitor(threading.Thread): input_records = arrtype() ReadConsoleInputW = KERNEL32.ReadConsoleInputW keys: List[str] = [] + append_key = keys.append while not exit_requested(): - if _wait_for_handles([hIn], 100) is None: + # Wait for new events + if _wait_for_handles([hIn], 200) is None: + # No new events continue - del keys[:] + + # Get new events ReadConsoleInputW( hIn, byref(input_records), MAX_EVENTS, byref(read_count) ) read_input_records = input_records[: read_count.value] - apppend_key = keys.append + 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": - apppend_key(key) + 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: diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py index 743d35b11..bc7a73c57 100644 --- a/src/textual/drivers/windows_driver.py +++ b/src/textual/drivers/windows_driver.py @@ -1,60 +1,17 @@ from __future__ import annotations import asyncio -from ctypes import windll -from ctypes.wintypes import BOOL, DWORD, HANDLE -from codecs import getincrementaldecoder - -import msvcrt -import os import sys from threading import Event, Thread -from typing import Callable, List, Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Callable -from ..driver import Driver -from ..geometry import Size - -from . import win32 # -from .. import events -from .. import log +from .._context import active_app from .._types import MessageTarget -from .._xterm_parser import XTermParser - +from ..driver import Driver +from . import win32 if TYPE_CHECKING: from rich.console import Console - from textual.app import App - -WAIT_TIMEOUT = 0x00000102 - - -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 = windll.kernel32.WaitForMultipleObjects( - len(handle_array), handle_array, BOOL(False), DWORD(timeout) - ) - - if ret == WAIT_TIMEOUT: - return None - else: - return handles[ret] class WindowsDriver(Driver): @@ -66,7 +23,6 @@ class WindowsDriver(Driver): self.out_fileno = sys.stdout.fileno() self.exit_event = Event() - self._key_thread: Thread | None = None self._event_thread: Thread | None = None self._restore_console: Callable[[], None] | None = None @@ -76,17 +32,12 @@ class WindowsDriver(Driver): write("\x1b[?1003h") # SET_ANY_EVENT_MOUSE write("\x1b[?1015h") # SET_VT200_HIGHLIGHT_MOUSE write("\x1b[?1006h") # SET_SGR_EXT_MODE_MOUSE - - # write("\x1b[?1007h") self.console.file.flush() - # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr - # extensions. - def _disable_mouse_support(self) -> None: write = self.console.file.write - write("\x1b[?1000l") # - write("\x1b[?1003l") # + write("\x1b[?1000l") + write("\x1b[?1003l") write("\x1b[?1015l") write("\x1b[?1006l") self.console.file.flush() @@ -102,27 +53,11 @@ class WindowsDriver(Driver): self.console.show_cursor(False) self.console.file.write("\033[?1003h\n") - from .._context import active_app - app = active_app.get() - # self._key_thread = Thread( - # target=self.run_input_thread, args=(asyncio.get_event_loop(), app) - # ) self._event_thread = win32.EventMonitor( loop, app, self._target, self.exit_event, self.process_event ) - width, height = os.get_terminal_size(self.out_fileno) - - asyncio.run_coroutine_threadsafe( - self._target.post_message(events.Resize(self._target, Size(width, height))), - loop=loop, - ) - - from .._context import active_app - - # self._key_thread.start() - self._event_thread.start() def disable_input(self) -> None: @@ -144,51 +79,3 @@ class WindowsDriver(Driver): with self.console: self.console.set_alt_screen(False) self.console.show_cursor(True) - - def run_input_thread(self, loop, app: App) -> None: - try: - self._run_input_thread(loop, app) - except Exception as error: - app.log(error) - - def _run_input_thread(self, loop, app: App) -> None: - app.log("input thread") - - parser = XTermParser(self._target, lambda: False) - - utf8_decoder = getincrementaldecoder("utf-8")().decode - decode = utf8_decoder - read = os.read - - input_handle = msvcrt.get_osfhandle(self.in_fileno) - app.log("input_handle", input_handle) - app.log("starting thread") - import time - - terminal_size = os.get_terminal_size(self.out_fileno) - import shutil - - try: - while not self.exit_event.is_set(): - - new_terminal_size = os.get_terminal_size(self.out_fileno) - - if new_terminal_size != terminal_size: - app.log("SIZE CHANGE", new_terminal_size) - terminal_size = new_terminal_size - width, height = new_terminal_size - event = events.Resize(self._target, Size(width, height)) - app.log(event) - self.console.size = (width, height) - self.send_event(event) - - if wait_for_handles([input_handle], 100) is None: - continue - unicode_data = decode(read(self.in_fileno, 1024)) - # app.log("key", repr(unicode_data)) - for event in parser.feed(unicode_data): - self.process_event(event) - except Exception as error: - app.log(error) - finally: - app.log("input thread finished") From 0e1ce3ab52d95c2c343481e03a1eae15865dc890 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Jan 2022 11:37:06 +0000 Subject: [PATCH 18/27] fix console --- src/textual/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index 0000f068c..95be8c07b 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -200,7 +200,7 @@ class App(MessagePump): """ 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()) From b6d7e93542b0381f57d611c1455c2d5e7ceb110a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 28 Jan 2022 15:30:59 +0000 Subject: [PATCH 19/27] UnderlineBar component --- src/textual/renderables/__init__.py | 0 src/textual/renderables/tab_underline.py | 99 ++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/textual/renderables/__init__.py create mode 100644 src/textual/renderables/tab_underline.py 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/tab_underline.py b/src/textual/renderables/tab_underline.py new file mode 100644 index 000000000..d2fc47838 --- /dev/null +++ b/src/textual/renderables/tab_underline.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import random + +from rich.color import Color, ANSI_COLOR_NAMES +from rich.console import ConsoleOptions, Console, RenderResult +from rich.segment import Segment +from rich.style import Style + + +class UnderlineBar: + def __init__( + self, + highlight_range: tuple[float, float] = 0, + range_color: Color = Color.parse("yellow"), + other_color: Color = Color.parse("default"), + background_color: Color = Color.parse("default"), + width: int | None = None, + ) -> None: + self.highlight_range = highlight_range + self.highlight_style = Style.from_color( + color=range_color, bgcolor=background_color + ) + self.other_style = Style.from_color(color=other_color, bgcolor=background_color) + self.width = width + + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + half_bar_right = "╸" + half_bar_left = "╺" + bar = "━" + width = self.width or options.max_width + start, end = self.highlight_range + + # Round start and end to nearest half + start = round(start * 2) / 2 + end = round(end * 2) / 2 + + # start = 3.6; end = 9.6 + # full bars before = int(start) = 3 + # half_start = .6 > 0 = True + + # Bars before highlighted range + half_start = start - int(start) > 0 + half_end = end - int(end) > 0 + + # Non-highlighted portion of bar + + if half_start: + yield Segment(bar * (int(start) + 1), style=self.other_style) + yield Segment(half_bar_left, style=self.highlight_style) + else: + yield Segment(bar * (int(start)), style=self.other_style) + yield Segment(half_bar_right, style=self.other_style) + + # If we have a half bar at start and end, we need 1 less full bar + full_bar_width = int(end) - int(start) + if half_start and half_end: + full_bar_width -= 1 + + yield Segment(bar * full_bar_width, style=self.highlight_style) + + if half_end: + yield Segment(half_bar_right, style=self.highlight_style) + else: + yield Segment(half_bar_left, style=self.other_style) + + yield Segment(bar * (int(width) - int(end)), style=self.other_style) + + +if __name__ == "__main__": + console = Console() + + def frange(start, end, step): + current = start + while current < end: + yield current + current += step + + while current >= 0: + yield current + current -= step + + start_range = frange(0, 12, 0.5) + end_range = frange(6, 18, 0.5) + ranges = zip(start_range, end_range) + + for range in ranges: + color = random.choice(list(ANSI_COLOR_NAMES.keys())) + console.print( + UnderlineBar( + range, + range_color=Color.parse(color), + other_color=Color.parse("#4f4f4f"), + width=18, + ) + ) + console.print() From 1b8d7d184e10889002425641222702afba508aea Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Jan 2022 16:14:13 +0000 Subject: [PATCH 20/27] version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 17bd50ac5..60d699196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.1.14" +version = "0.1.15" homepage = "https://github.com/willmcgugan/textual" description = "Text User Interface using Rich" authors = ["Will McGugan "] From 9e10f38d6b55e0ac38db41ef64fbb0e367381784 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 28 Jan 2022 16:55:09 +0000 Subject: [PATCH 21/27] Fixing issues with start and end of bar --- src/textual/renderables/tab_underline.py | 30 ++++++++++-------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/textual/renderables/tab_underline.py b/src/textual/renderables/tab_underline.py index d2fc47838..25de4b893 100644 --- a/src/textual/renderables/tab_underline.py +++ b/src/textual/renderables/tab_underline.py @@ -30,28 +30,19 @@ class UnderlineBar: half_bar_right = "╸" half_bar_left = "╺" bar = "━" - width = self.width or options.max_width + width = (self.width or options.max_width) - 1 start, end = self.highlight_range # Round start and end to nearest half start = round(start * 2) / 2 end = round(end * 2) / 2 - # start = 3.6; end = 9.6 - # full bars before = int(start) = 3 - # half_start = .6 > 0 = True - - # Bars before highlighted range half_start = start - int(start) > 0 half_end = end - int(end) > 0 - # Non-highlighted portion of bar - - if half_start: - yield Segment(bar * (int(start) + 1), style=self.other_style) - yield Segment(half_bar_left, style=self.highlight_style) - else: - yield Segment(bar * (int(start)), style=self.other_style) + # Initial non-highlighted portion of bar + yield Segment(bar * (int(start - 0.5)), style=self.other_style) + if not half_start and start > 0: yield Segment(half_bar_right, style=self.other_style) # If we have a half bar at start and end, we need 1 less full bar @@ -59,13 +50,18 @@ class UnderlineBar: if half_start and half_end: full_bar_width -= 1 - yield Segment(bar * full_bar_width, style=self.highlight_style) - + # The highlighted portion + if not half_start: + yield Segment(bar * full_bar_width, style=self.highlight_style) + else: + yield Segment(half_bar_left, style=self.highlight_style) + yield Segment(bar * full_bar_width, style=self.highlight_style) if half_end: yield Segment(half_bar_right, style=self.highlight_style) - else: - yield Segment(half_bar_left, style=self.other_style) + # The non-highlighted tail + if not half_end and end - width != 1: + yield Segment(half_bar_left, style=self.other_style) yield Segment(bar * (int(width) - int(end)), style=self.other_style) From b21a01dfe166fc7ea1f4827dc35a0993122d4be0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 30 Jan 2022 14:30:53 +0000 Subject: [PATCH 22/27] Warning about changes --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8b3f02849..3dadeb019 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. From 5651e97a64b850b80f42799e7f7d868f1f11ab7b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jan 2022 13:03:48 +0000 Subject: [PATCH 23/27] Underline bar renderable --- src/textual/renderables/tab_underline.py | 95 ------------------- src/textual/renderables/underline_bar.py | 112 +++++++++++++++++++++++ tests/renderables/__init__.py | 0 tests/renderables/test_underline_bar.py | 98 ++++++++++++++++++++ tests/utilities/render.py | 24 +++++ 5 files changed, 234 insertions(+), 95 deletions(-) delete mode 100644 src/textual/renderables/tab_underline.py create mode 100644 src/textual/renderables/underline_bar.py create mode 100644 tests/renderables/__init__.py create mode 100644 tests/renderables/test_underline_bar.py create mode 100644 tests/utilities/render.py diff --git a/src/textual/renderables/tab_underline.py b/src/textual/renderables/tab_underline.py deleted file mode 100644 index 25de4b893..000000000 --- a/src/textual/renderables/tab_underline.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import annotations - -import random - -from rich.color import Color, ANSI_COLOR_NAMES -from rich.console import ConsoleOptions, Console, RenderResult -from rich.segment import Segment -from rich.style import Style - - -class UnderlineBar: - def __init__( - self, - highlight_range: tuple[float, float] = 0, - range_color: Color = Color.parse("yellow"), - other_color: Color = Color.parse("default"), - background_color: Color = Color.parse("default"), - width: int | None = None, - ) -> None: - self.highlight_range = highlight_range - self.highlight_style = Style.from_color( - color=range_color, bgcolor=background_color - ) - self.other_style = Style.from_color(color=other_color, bgcolor=background_color) - self.width = width - - def __rich_console__( - self, console: Console, options: ConsoleOptions - ) -> RenderResult: - half_bar_right = "╸" - half_bar_left = "╺" - bar = "━" - width = (self.width or options.max_width) - 1 - start, end = self.highlight_range - - # Round start and end to nearest half - start = round(start * 2) / 2 - end = round(end * 2) / 2 - - 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=self.other_style) - if not half_start and start > 0: - yield Segment(half_bar_right, style=self.other_style) - - # If we have a half bar at start and end, we need 1 less full bar - full_bar_width = int(end) - int(start) - if half_start and half_end: - full_bar_width -= 1 - - # The highlighted portion - if not half_start: - yield Segment(bar * full_bar_width, style=self.highlight_style) - else: - yield Segment(half_bar_left, style=self.highlight_style) - yield Segment(bar * full_bar_width, style=self.highlight_style) - if half_end: - yield Segment(half_bar_right, style=self.highlight_style) - - # The non-highlighted tail - if not half_end and end - width != 1: - yield Segment(half_bar_left, style=self.other_style) - yield Segment(bar * (int(width) - int(end)), style=self.other_style) - - -if __name__ == "__main__": - console = Console() - - def frange(start, end, step): - current = start - while current < end: - yield current - current += step - - while current >= 0: - yield current - current -= step - - start_range = frange(0, 12, 0.5) - end_range = frange(6, 18, 0.5) - ranges = zip(start_range, end_range) - - for range in ranges: - color = random.choice(list(ANSI_COLOR_NAMES.keys())) - console.print( - UnderlineBar( - range, - range_color=Color.parse(color), - other_color=Color.parse("#4f4f4f"), - width=18, - ) - ) - console.print() diff --git a/src/textual/renderables/underline_bar.py b/src/textual/renderables/underline_bar.py new file mode 100644 index 000000000..6e5aa532f --- /dev/null +++ b/src/textual/renderables/underline_bar.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import random + +from rich.color import Color, ANSI_COLOR_NAMES +from rich.console import ConsoleOptions, Console, RenderResult +from rich.segment import Segment +from rich.style import Style + + +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_color (Color | str): The color of the highlighted range of the bar. + non_highlight_color (Color | str): The color of the non-highlighted range(s) of the bar. + background_color (Color | str): The background color of the entire 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_color: Color | str = "magenta", + non_highlight_color: Color | str = "grey37", + background_color: Color | str = "default", + width: int | None = None, + ) -> None: + self.highlight_range = highlight_range + self.highlight_style = Style(color=highlight_color, bgcolor=background_color) + self.non_highlight_style = Style( + color=non_highlight_color, bgcolor=background_color + ) + self.width = width + + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + 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: + yield Segment(bar * width, style=self.non_highlight_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=self.non_highlight_style) + if not half_start and start > 0: + yield Segment(half_bar_right, style=self.non_highlight_style) + + # The highlighted portion + bar_width = int(end) - int(start) + if half_start: + yield Segment( + half_bar_left + bar * (bar_width - 1), style=self.highlight_style + ) + else: + yield Segment(bar * bar_width, style=self.highlight_style) + if half_end: + yield Segment(half_bar_right, style=self.highlight_style) + + # The non-highlighted tail + if not half_end and end - width != 0: + yield Segment(half_bar_left, style=self.non_highlight_style) + yield Segment(bar * (int(width) - int(end) - 1), style=self.non_highlight_style) + + +if __name__ == "__main__": + 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.5 + 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_color=Color.parse(color), + width=20, + ), + f" {range}", + ) 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..c12256c66 --- /dev/null +++ b/tests/renderables/test_underline_bar.py @@ -0,0 +1,98 @@ +from tests.utilities.render import render +from textual.renderables.underline_bar import UnderlineBar + +MAGENTA = "\x1b[35;49m" +GREY = "\x1b[38;5;59;49m" +STOP = "\x1b[0m" + + +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}" + ) 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 From 42585941519716bc742455e8f4113db5e713856e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 31 Jan 2022 13:58:37 +0000 Subject: [PATCH 24/27] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8feba707c..3416fd003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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] - Unreleased +## [1.1.15] - 2022-01-31 ### Added From cfefb36ee4dcd4ac3b5c5bde5c040ebeb55f11e6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 31 Jan 2022 14:00:54 +0000 Subject: [PATCH 25/27] Update for windows --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3dadeb019..866cba6f9 100644 --- a/README.md +++ b/README.md @@ -14,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 From 776284ddd0e2075d40a3cae7d965159f2daf9ec4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jan 2022 14:02:16 +0000 Subject: [PATCH 26/27] Ensure we clamp range properly, passing Styles directly --- src/textual/renderables/underline_bar.py | 59 +++++++++++++++--------- tests/renderables/test_underline_bar.py | 28 ++++++++++- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/textual/renderables/underline_bar.py b/src/textual/renderables/underline_bar.py index 6e5aa532f..357b340d8 100644 --- a/src/textual/renderables/underline_bar.py +++ b/src/textual/renderables/underline_bar.py @@ -1,11 +1,8 @@ from __future__ import annotations -import random - -from rich.color import Color, ANSI_COLOR_NAMES from rich.console import ConsoleOptions, Console, RenderResult from rich.segment import Segment -from rich.style import Style +from rich.style import Style, StyleType class UnderlineBar: @@ -13,25 +10,27 @@ class UnderlineBar: Args: highlight_range (tuple[float, float]): The range to highlight. Defaults to ``(0, 0)`` (no highlight) - highlight_color (Color | str): The color of the highlighted range of the bar. - non_highlight_color (Color | str): The color of the non-highlighted range(s) of the bar. - background_color (Color | str): The background color of the entire bar. + 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_color: Color | str = "magenta", - non_highlight_color: Color | str = "grey37", - background_color: Color | str = "default", + highlight_style: StyleType = "magenta", + background_style: StyleType = "grey37", width: int | None = None, ) -> None: self.highlight_range = highlight_range - self.highlight_style = Style(color=highlight_color, bgcolor=background_color) - self.non_highlight_style = Style( - color=non_highlight_color, bgcolor=background_color - ) + if isinstance(highlight_style, str): + self.highlight_style = Style.parse(highlight_style) + else: + self.highlight_style = highlight_style + if isinstance(background_style, str): + self.background_style = Style.parse(background_style) + else: + self.background_style = background_style self.width = width def __rich_console__( @@ -40,14 +39,15 @@ class UnderlineBar: 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: - yield Segment(bar * width, style=self.non_highlight_style) + if start == end == 0 or end < 0 or start > end: + yield Segment(bar * width, style=self.background_style) return # Round start and end to nearest half @@ -59,9 +59,9 @@ class UnderlineBar: half_end = end - int(end) > 0 # Initial non-highlighted portion of bar - yield Segment(bar * (int(start - 0.5)), style=self.non_highlight_style) + yield Segment(bar * (int(start - 0.5)), style=self.background_style) if not half_start and start > 0: - yield Segment(half_bar_right, style=self.non_highlight_style) + yield Segment(half_bar_right, style=self.background_style) # The highlighted portion bar_width = int(end) - int(start) @@ -76,11 +76,15 @@ class UnderlineBar: # The non-highlighted tail if not half_end and end - width != 0: - yield Segment(half_bar_left, style=self.non_highlight_style) - yield Segment(bar * (int(width) - int(end) - 1), style=self.non_highlight_style) + yield Segment(half_bar_left, style=self.background_style) + yield Segment(bar * (int(width) - int(end) - 1), style=self.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): @@ -93,7 +97,7 @@ if __name__ == "__main__": yield current current -= step - step = 0.5 + step = 0.1 start_range = frange(0.5, 10.5, step) end_range = frange(10, 20, step) ranges = zip(start_range, end_range) @@ -105,8 +109,19 @@ if __name__ == "__main__": console.print( UnderlineBar( range, - highlight_color=Color.parse(color), + 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/test_underline_bar.py b/tests/renderables/test_underline_bar.py index c12256c66..511b84f2b 100644 --- a/tests/renderables/test_underline_bar.py +++ b/tests/renderables/test_underline_bar.py @@ -1,8 +1,10 @@ +from rich.style import Style + from tests.utilities.render import render from textual.renderables.underline_bar import UnderlineBar -MAGENTA = "\x1b[35;49m" -GREY = "\x1b[38;5;59;49m" +MAGENTA = "\x1b[35m" +GREY = "\x1b[38;5;59m" STOP = "\x1b[0m" @@ -96,3 +98,25 @@ def test_highlight_out_of_bounds_end(): 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_init_with_str_style(): + bar = UnderlineBar(background_style="green", highlight_style="yellow") + assert bar.background_style == Style(color="green") + assert bar.highlight_style == Style(color="yellow") + + +def test_init_with_object_style(): + bar = UnderlineBar(background_style=Style(color="green"), highlight_style=Style(color="yellow")) + assert bar.background_style == Style(color="green") + assert bar.highlight_style == Style(color="yellow") From 65eb1b0b4f90682869f54ddac6ca2881f4b7ad2d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jan 2022 14:13:49 +0000 Subject: [PATCH 27/27] Using console.get_style, adding test ensuring style applies --- src/textual/renderables/underline_bar.py | 33 ++++++++++-------------- tests/renderables/test_underline_bar.py | 28 +++++++++++--------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/textual/renderables/underline_bar.py b/src/textual/renderables/underline_bar.py index 357b340d8..59c9e6bb4 100644 --- a/src/textual/renderables/underline_bar.py +++ b/src/textual/renderables/underline_bar.py @@ -2,7 +2,7 @@ from __future__ import annotations from rich.console import ConsoleOptions, Console, RenderResult from rich.segment import Segment -from rich.style import Style, StyleType +from rich.style import StyleType class UnderlineBar: @@ -23,19 +23,16 @@ class UnderlineBar: width: int | None = None, ) -> None: self.highlight_range = highlight_range - if isinstance(highlight_style, str): - self.highlight_style = Style.parse(highlight_style) - else: - self.highlight_style = highlight_style - if isinstance(background_style, str): - self.background_style = Style.parse(background_style) - else: - self.background_style = background_style + 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 = "━" @@ -47,7 +44,7 @@ class UnderlineBar: end = min(end, width) if start == end == 0 or end < 0 or start > end: - yield Segment(bar * width, style=self.background_style) + yield Segment(bar * width, style=background_style) return # Round start and end to nearest half @@ -59,25 +56,23 @@ class UnderlineBar: half_end = end - int(end) > 0 # Initial non-highlighted portion of bar - yield Segment(bar * (int(start - 0.5)), style=self.background_style) + yield Segment(bar * (int(start - 0.5)), style=background_style) if not half_start and start > 0: - yield Segment(half_bar_right, style=self.background_style) + 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=self.highlight_style - ) + yield Segment(half_bar_left + bar * (bar_width - 1), style=highlight_style) else: - yield Segment(bar * bar_width, style=self.highlight_style) + yield Segment(bar * bar_width, style=highlight_style) if half_end: - yield Segment(half_bar_right, style=self.highlight_style) + 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=self.background_style) - yield Segment(bar * (int(width) - int(end) - 1), style=self.background_style) + yield Segment(half_bar_left, style=background_style) + yield Segment(bar * (int(width) - int(end) - 1), style=background_style) if __name__ == "__main__": diff --git a/tests/renderables/test_underline_bar.py b/tests/renderables/test_underline_bar.py index 511b84f2b..5c5e4de9c 100644 --- a/tests/renderables/test_underline_bar.py +++ b/tests/renderables/test_underline_bar.py @@ -1,11 +1,11 @@ -from rich.style import Style - 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(): @@ -110,13 +110,17 @@ def test_highlight_full_range_out_of_bounds_start(): assert render(bar) == f"{GREY}━━━━━━{STOP}" -def test_init_with_str_style(): - bar = UnderlineBar(background_style="green", highlight_style="yellow") - assert bar.background_style == Style(color="green") - assert bar.highlight_style == Style(color="yellow") - - -def test_init_with_object_style(): - bar = UnderlineBar(background_style=Style(color="green"), highlight_style=Style(color="yellow")) - assert bar.background_style == Style(color="green") - assert bar.highlight_style == Style(color="yellow") +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}" + )