diff --git a/.gitignore b/.gitignore index f97bd86a6..d0620ea4a 100644 --- a/.gitignore +++ b/.gitignore @@ -110,6 +110,9 @@ venv.bak/ # mkdocs documentation /site +/docs-offline +/mkdocs-nav-online.yml +/mkdocs-nav-offline.yml # mypy .mypy_cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5270475ee..950c16049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637 - `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471 -- Breaking change: renamed `Checkbox` to `Switch`. - `DataTable.add_row` now accepts `key` argument to uniquely identify the row https://github.com/Textualize/textual/pull/1638 - `DataTable.add_column` now accepts `key` argument to uniquely identify the column https://github.com/Textualize/textual/pull/1638 - `DataTable.add_row` and `DataTable.add_column` now return lists of keys identifying the added rows/columns https://github.com/Textualize/textual/pull/1638 @@ -52,6 +51,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Breaking change: `DataTable.refresh_cell` was renamed to `DataTable.refresh_coordinate` https://github.com/Textualize/textual/pull/1638 - Breaking change: `DataTable.get_row_height` now takes a `RowKey` argument instead of a row index https://github.com/Textualize/textual/pull/1638 - The `_filter` module was made public (now called `filter`) https://github.com/Textualize/textual/pull/1638 +- Breaking change: renamed `Checkbox` to `Switch` https://github.com/Textualize/textual/issues/1746 ### Fixed diff --git a/Makefile b/Makefile index 9d71e69d7..de1bea195 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,78 @@ +run := poetry run + +.PHONY: test test: - pytest --cov-report term-missing --cov=textual tests/ -vv + $(run) pytest --cov-report term-missing --cov=textual tests/ -vv + +.PHONY: unit-test unit-test: - pytest --cov-report term-missing --cov=textual tests/ -vv -m "not integration_test" + $(run) pytest --cov-report term-missing --cov=textual tests/ -vv -m "not integration_test" + +.PHONY: test-snapshot-update test-snapshot-update: - pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update + $(run) pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update + +.PHONY: typecheck typecheck: - mypy src/textual + $(run) mypy src/textual + +.PHONY: format format: - black src + $(run) black src + +.PHONY: format-check format-check: - black --check src -docs-serve: + $(run) black --check src + +.PHONY: clean-screenshot-cache +clean-screenshot-cache: rm -rf .screenshot_cache - mkdocs serve -docs-build: - mkdocs build -docs-deploy: - rm -rf .screenshot_cache - mkdocs gh-deploy + +.PHONY: docs-offline-nav +docs-offline-nav: + echo "INHERIT: mkdocs-offline.yml" > mkdocs-nav-offline.yml + grep -v "\- \"*[Bb]log" mkdocs-nav.yml >> mkdocs-nav-offline.yml + +.PHONY: docs-online-nav +docs-online-nav: + echo "INHERIT: mkdocs-online.yml" > mkdocs-nav-online.yml + cat mkdocs-nav.yml >> mkdocs-nav-online.yml + +.PHONY: docs-serve +docs-serve: clean-screenshot-cache docs-online-nav + $(run) mkdocs serve --config-file mkdocs-nav-online.yml + rm -f mkdocs-nav-online.yml + +.PHONY: docs-build +docs-build: docs-online-nav + $(run) mkdocs build --config-file mkdocs-nav-online.yml + rm -f mkdocs-nav-online.yml + +.PHONY: docs-build-offline +docs-build-offline: docs-offline-nav + $(run) mkdocs build --config-file mkdocs-nav-offline.yml + rm -f mkdocs-nav-offline.yml + +.PHONY: clean-offline-docs +clean-offline-docs: + rm -rf docs-offline + +.PHONY: docs-deploy +docs-deploy: clean-screenshot-cache docs-online-nav + $(run) mkdocs gh-deploy --config-file mkdocs-nav-online.yml + rm -f mkdocs-nav-online.yml + +.PHONY: build +build: docs-build-offline + poetry build + +.PHONY: clean +clean: clean-screenshot-cache clean-offline-docs + +.PHONY: setup +setup: + poetry install --extras dev + +.PHONY: update +update: + poetry update diff --git a/README.md b/README.md index 3bdf8ffc4..587bfca33 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ ![Textual splash image](https://raw.githubusercontent.com/Textualize/textual/main/imgs/textual.png) -Textual is a Python framework for creating interactive applications that run in your terminal. +Textual is a *Rapid Application Development* framework for Python. + +Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and (coming soon) a web browser!
🎬 Demonstration @@ -22,7 +24,7 @@ https://user-images.githubusercontent.com/554369/197355913-65d3c125-493d-4c05-a5 ## About -Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with a Python API inspired by modern web development. +Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with an API inspired by modern web development. On modern terminal software (installed by default on most systems), Textual apps can use **16.7 million** colors with mouse support and smooth flicker-free animation. A powerful layout engine and re-usable components makes it possible to build apps that rival the desktop and web experience. diff --git a/docs/blog/images/async-create-task.jpeg b/docs/blog/images/async-create-task.jpeg new file mode 100644 index 000000000..7a06ce333 Binary files /dev/null and b/docs/blog/images/async-create-task.jpeg differ diff --git a/docs/blog/posts/create-task-psa.md b/docs/blog/posts/create-task-psa.md new file mode 100644 index 000000000..737316bf2 --- /dev/null +++ b/docs/blog/posts/create-task-psa.md @@ -0,0 +1,28 @@ +--- +draft: false +date: 2023-02-11 +categories: + - DevLog +authors: + - willmcgugan +--- + +# The Heisenbug lurking in your async code + +I'm taking a brief break from blogging about [Textual](https://github.com/Textualize/textual) to bring you this brief PSA for Python developers who work with async code. I wanted to expand a little on this [tweet](https://twitter.com/willmcgugan/status/1624419352211603461). + + + +If you have ever used `asyncio.create_task` you may have created a bug for yourself that is challenging (read *almost impossible*) to reproduce. If it occurs, your code will likely fail in unpredictable ways. + +The root cause of this [Heisenbug](https://en.wikipedia.org/wiki/Heisenbug) is that if you don't hold a reference to the task object returned by `create_task` then the task may disappear without warning when Python runs garbage collection. In other words, the code in your task will stop running with no obvious indication why. + +This behavior is [well documented](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task), as you can see from this excerpt (emphasis mine): + +![create task](../images/async-create-task.jpeg) + +But who reads all the docs? And who has perfect recall if they do? A search on GitHub indicates that there are a [lot of projects](https://github.com/search?q=%22asyncio.create_task%28%22&type=code) where this bug is waiting for just the right moment to ruin somebody's day. + +I suspect the reason this mistake is so common is that tasks are a lot like threads (conceptually at least). With threads you can just launch them and forget. Unless you mark them as "daemon" threads they will exist for the lifetime of your app. Not so with Tasks. + +The solution recommended in the docs is to keep a reference to the task for as long as you need it to live. On modern Python you could use [TaskGroups](https://docs.python.org/3/library/asyncio-task.html#task-groups) which will keep references to your tasks. As long as all the tasks you spin up are in TaskGroups, you should be fine. diff --git a/docs/examples/guide/screens/modal01.css b/docs/examples/guide/screens/modal01.css index 8c7bea071..a6c01de21 100644 --- a/docs/examples/guide/screens/modal01.css +++ b/docs/examples/guide/screens/modal01.css @@ -1,7 +1,7 @@ #dialog { grid-size: 2; grid-gutter: 1 2; - margin: 1 2; + padding: 1 2; } #question { diff --git a/docs/examples/widgets/switch.css b/docs/examples/widgets/switch.css index fb6a0d220..20fd57ba8 100644 --- a/docs/examples/widgets/switch.css +++ b/docs/examples/widgets/switch.css @@ -22,7 +22,7 @@ Switch { background: darkslategrey; } -#custom-design > .switch--switch { +#custom-design > .switch--slider { color: dodgerblue; background: darkslateblue; } diff --git a/docs/guide/events.md b/docs/guide/events.md index cefe87de2..7c3df74c4 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -183,7 +183,7 @@ Let's look at an example which looks up word definitions from an [api](https://d === "Output" - ```{.textual path="docs/examples/events/dictionary.py" press="t,e,x,t,_,_,_,_,_,_,_,_,_,_,_"} + ```{.textual path="docs/examples/events/dictionary.py"} ``` Note the highlighted line in the above code which calls `asyncio.create_task` to run a coroutine in the background. Without this you would find typing in to the text box to be unresponsive. diff --git a/docs/index.md b/docs/index.md index 4d1c05832..1ce69322d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -81,10 +81,6 @@ Build sophisticated user interfaces with a simple Python API. Run your apps in t ``` -```{.textual path="docs/examples/events/dictionary.py" columns="100" lines="30" press="tab,_,t,e,x,t,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_"} -``` - - ```{.textual path="docs/examples/guide/layout/combining_layouts.py" columns="100", lines="30"} ``` diff --git a/hatch.toml b/hatch.toml deleted file mode 100644 index 1e25a12b1..000000000 --- a/hatch.toml +++ /dev/null @@ -1,38 +0,0 @@ -[envs.default] -features = ["dev"] -dependencies = [ - "jinja2 < 3.1.0", - "pytest >= 7.1.3, <8", - "pytest-aiohttp >= 1.0.4, <2", - "pytest-cov >= 2.12.1, <3", - "syrupy >= 3.0.0, <4", - "time-machine >= 2.6.0, <3", -] -[envs.default.scripts] -test = "pytest --cov-report term-missing --cov=textual tests/ -vv" -test-snapshot-update = "pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update" -unit-test = 'pytest --cov-report term-missing --cov=textual tests/ -vv -m "not integration_test"' - -[envs.lint] -detached = true -dependencies = [ - "black >= 22.3.0, <23", - "mypy >= 0.982, <1", - "pre-commit >= 2.12.1, <3", -] -[envs.lint.scripts] -format = "black src" -check = "black --check src" -typing = "mypy src/textual" - -[envs.docs] -dependencies = [ - "mkdocs >= 1.3.0, <2", - "mkdocs-material >= 8.2.15, <9", - "mkdocstrings[python] >= 0.19.0, <1", -] -[envs.docs.scripts] -build = "mkdocs build" -deploy = "mkdocs gh-deploy" -serve = "mkdocs serve" -help = "mkdocs --help" diff --git a/mkdocs-common.yml b/mkdocs-common.yml new file mode 100644 index 000000000..5b21dd4fc --- /dev/null +++ b/mkdocs-common.yml @@ -0,0 +1,100 @@ +site_name: Textual + +markdown_extensions: + - attr_list + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg + - md_in_html + - admonition + - def_list + - meta + + - toc: + permalink: true + baselevel: 1 + - pymdownx.keys + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.superfences: + custom_fences: + - name: textual + class: textual + format: !!python/name:textual._doc.format_svg + - name: rich + class: rich + format: !!python/name:textual._doc.rich + - pymdownx.inlinehilite + - pymdownx.superfences + - pymdownx.snippets + - pymdownx.tabbed: + alternate_style: true + - pymdownx.snippets + - markdown.extensions.attr_list + +theme: + name: material + custom_dir: docs/custom_theme + features: + - navigation.tabs + - navigation.indexes + - navigation.tabs.sticky + - navigation.footer + - content.code.annotate + - content.code.copy + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + accent: purple + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + toggle: + icon: material/weather-night + name: Switch to light mode + +plugins: + search: + autorefs: + mkdocstrings: + custom_templates: docs/_templates + default_handler: python + handlers: + python: + options: + show_root_heading: true + show_root_full_path: false + show_source: false + filters: + - "!^_" + - "^__init__$" + - "!^can_replace$" + watch: + - src/textual + exclude: + glob: + - "**/_template.md" + + +extra_css: + - stylesheets/custom.css + + +extra: + social: + - icon: fontawesome/brands/twitter + link: https://twitter.com/textualizeio + name: textualizeio on Twitter + - icon: fontawesome/brands/github + link: https://github.com/textualize/textual/ + name: Textual on Github + - icon: fontawesome/brands/discord + link: https://discord.gg/Enf6Z3qhVr + name: Textual Discord server +copyright: Copyright © Textualize, Inc diff --git a/mkdocs.yml b/mkdocs-nav.yml similarity index 65% rename from mkdocs.yml rename to mkdocs-nav.yml index 863f1288d..6b5c4508b 100644 --- a/mkdocs.yml +++ b/mkdocs-nav.yml @@ -1,8 +1,3 @@ -site_name: Textual -site_url: https://textual.textualize.io/ -repo_url: https://github.com/textualize/textual/ -edit_uri: edit/main/docs/ - nav: - Introduction: - "index.md" @@ -178,114 +173,3 @@ nav: - "api/widget.md" - "Blog": - blog/index.md - - - -markdown_extensions: - - attr_list - - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg - - md_in_html - - admonition - - def_list - - meta - - - toc: - permalink: true - baselevel: 1 - - pymdownx.keys - - pymdownx.tasklist: - custom_checkbox: true - - pymdownx.highlight: - anchor_linenums: true - - pymdownx.inlinehilite - - pymdownx.superfences: - custom_fences: - - name: textual - class: textual - format: !!python/name:textual._doc.format_svg - - name: rich - class: rich - format: !!python/name:textual._doc.rich - - pymdownx.inlinehilite - - pymdownx.superfences - - pymdownx.snippets - - pymdownx.tabbed: - alternate_style: true - - pymdownx.snippets - - markdown.extensions.attr_list - -theme: - name: material - custom_dir: docs/custom_theme - features: - - navigation.tabs - - navigation.indexes - - navigation.tabs.sticky - - navigation.footer - - content.code.annotate - - content.code.copy - palette: - - media: "(prefers-color-scheme: light)" - scheme: default - accent: purple - toggle: - icon: material/weather-sunny - name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: black - toggle: - icon: material/weather-night - name: Switch to light mode - -plugins: - -- blog: -- rss: - match_path: blog/posts/.* - date_from_meta: - as_creation: date - categories: - - categories - - release - - tags -- search: -- autorefs: -- mkdocstrings: - custom_templates: docs/_templates - default_handler: python - handlers: - python: - options: - show_root_heading: true - show_root_full_path: false - show_source: false - filters: - - "!^_" - - "^__init__$" - - "!^can_replace$" - watch: - - src/textual -- exclude: - glob: - - "**/_template.md" - - -extra_css: - - stylesheets/custom.css - - -extra: - social: - - icon: fontawesome/brands/twitter - link: https://twitter.com/textualizeio - name: textualizeio on Twitter - - icon: fontawesome/brands/github - link: https://github.com/textualize/textual/ - name: Textual on Github - - icon: fontawesome/brands/discord - link: https://discord.gg/Enf6Z3qhVr - name: Textual Discord server -copyright: Copyright © Textualize, Inc diff --git a/mkdocs-offline.yml b/mkdocs-offline.yml new file mode 100644 index 000000000..0d367af42 --- /dev/null +++ b/mkdocs-offline.yml @@ -0,0 +1,11 @@ +INHERIT: mkdocs-common.yml + +plugins: + offline: + privacy: + exclude: + glob: + - "**/_template.md" + - blog/* + +site_dir: docs-offline diff --git a/mkdocs-online.yml b/mkdocs-online.yml new file mode 100644 index 000000000..58aea3fc7 --- /dev/null +++ b/mkdocs-online.yml @@ -0,0 +1,16 @@ +INHERIT: mkdocs-common.yml + +repo_url: https://github.com/textualize/textual/ +site_url: https://textual.textualize.io/ +edit_uri: edit/main/docs/ + +plugins: + blog: + rss: + match_path: blog/posts/.* + date_from_meta: + as_creation: date + categories: + - categories + - release + - tags diff --git a/poetry.lock b/poetry.lock index a2448ef8b..9cbbcc5db 100644 --- a/poetry.lock +++ b/poetry.lock @@ -490,19 +490,21 @@ mkdocs = "*" [[package]] name = "mkdocs-material" -version = "8.5.11" +version = "9.0.11" description = "Documentation that simply works" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -jinja2 = ">=3.0.2" +colorama = ">=0.4" +jinja2 = ">=3.0" markdown = ">=3.2" -mkdocs = ">=1.4.0" +mkdocs = ">=1.4.2" mkdocs-material-extensions = ">=1.1" -pygments = ">=2.12" -pymdown-extensions = ">=9.4" +pygments = ">=2.14" +pymdown-extensions = ">=9.9.1" +regex = ">=2022.4.24" requests = ">=2.26" [[package]] @@ -654,7 +656,7 @@ python-versions = ">=3.7" [[package]] name = "platformdirs" -version = "2.6.2" +version = "3.0.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -664,8 +666,8 @@ python-versions = ">=3.7" typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -827,6 +829,14 @@ python-versions = ">=3.6" [package.dependencies] pyyaml = "*" +[[package]] +name = "regex" +version = "2022.10.31" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "requests" version = "2.28.2" @@ -959,6 +969,25 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "types-docutils" +version = "0.19.1.3" +description = "Typing stubs for docutils" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-setuptools" +version = "67.2.0.1" +description = "Typing stubs for setuptools" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +types-docutils = "*" + [[package]] name = "typing-extensions" version = "4.4.0" @@ -990,7 +1019,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.18.0" +version = "20.19.0" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1000,7 +1029,7 @@ python-versions = ">=3.7" distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} -platformdirs = ">=2.4,<3" +platformdirs = ">=2.4,<4" [package.extras] docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] @@ -1032,7 +1061,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.12.1" +version = "3.13.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1048,7 +1077,7 @@ dev = ["aiohttp", "click", "msgpack"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "425fac5cc893af33128a6baf4fc9e296781d322e64eea5d9341d1265c6637d3c" +content-hash = "857b1469b81d325df7d76ed9c727e8e0943514be4dae1c0989b65d8e655ab2f2" [metadata.files] aiohttp = [ @@ -1483,8 +1512,8 @@ mkdocs-exclude = [ {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"}, ] mkdocs-material = [ - {file = "mkdocs_material-8.5.11-py3-none-any.whl", hash = "sha256:c907b4b052240a5778074a30a78f31a1f8ff82d7012356dc26898b97559f082e"}, - {file = "mkdocs_material-8.5.11.tar.gz", hash = "sha256:b0ea0513fd8cab323e8a825d6692ea07fa83e917bb5db042e523afecc7064ab7"}, + {file = "mkdocs_material-9.0.11-py3-none-any.whl", hash = "sha256:90a1e1ed41e90de5d0ab97c874b7bf6af488d0faf4aaea8e5868e01f3f1ed923"}, + {file = "mkdocs_material-9.0.11.tar.gz", hash = "sha256:aff49e4ce622a107ed563b3a6a37dc3660a45a0e4d9e7d4d2c13ce9dc02a7faf"}, ] mkdocs-material-extensions = [ {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, @@ -1685,8 +1714,8 @@ pathspec = [ {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, ] platformdirs = [ - {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, - {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, + {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, + {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1774,6 +1803,96 @@ 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"}, ] +regex = [ + {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, + {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, + {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, + {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, + {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, + {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, + {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, + {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, + {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, + {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, + {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, + {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, + {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, + {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, + {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, + {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, + {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, +] requests = [ {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, @@ -1895,6 +2014,14 @@ typed-ast = [ {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] +types-docutils = [ + {file = "types-docutils-0.19.1.3.tar.gz", hash = "sha256:36fe30de56f1ece1a9f7a990d47daa781b5af831d2b3f2dcb7dfd01b857cc3d4"}, + {file = "types_docutils-0.19.1.3-py3-none-any.whl", hash = "sha256:d608e6b91ccf0e8e01c586a0af5b0e0462382d3be65b734af82d40c9d010735d"}, +] +types-setuptools = [ + {file = "types-setuptools-67.2.0.1.tar.gz", hash = "sha256:07648088bc2cbf0f2745107d394e619ba2a747f68a5904e6e4089c0cb8322065"}, + {file = "types_setuptools-67.2.0.1-py3-none-any.whl", hash = "sha256:f15b2924122dca5f99a9f6a96a872145721373fe1bb6d656cf269c2a8b73a74b"}, +] typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, @@ -1908,8 +2035,8 @@ urllib3 = [ {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, ] virtualenv = [ - {file = "virtualenv-20.18.0-py3-none-any.whl", hash = "sha256:9d61e4ec8d2c0345dab329fb825eb05579043766a4b26a2f66b28948de68c722"}, - {file = "virtualenv-20.18.0.tar.gz", hash = "sha256:f262457a4d7298a6b733b920a196bf8b46c8af15bf1fd9da7142995eff15118e"}, + {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, + {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, ] watchdog = [ {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"}, @@ -2018,6 +2145,6 @@ yarl = [ {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, ] zipp = [ - {file = "zipp-3.12.1-py3-none-any.whl", hash = "sha256:6c4fe274b8f85ec73c37a8e4e3fa00df9fb9335da96fb789e3b96b318e5097b3"}, - {file = "zipp-3.12.1.tar.gz", hash = "sha256:a3cac813d40993596b39ea9e93a18e8a2076d5c378b8bc88ec32ab264e04ad02"}, + {file = "zipp-3.13.0-py3-none-any.whl", hash = "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"}, + {file = "zipp-3.13.0.tar.gz", hash = "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6"}, ] diff --git a/pyproject.toml b/pyproject.toml index 1d8d28a24..7b627f0af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,13 @@ classifiers = [ include = [ "src/textual/py.typed", { path = "docs/examples", format = "sdist" }, - { path = "tests", format = "sdist" } + { path = "tests", format = "sdist" }, + # The reason for the slightly convoluted path specification here is that + # poetry populates the exclude list with the content of .gitignore, and + # it also seems like exclude trumps include. So here we specify that we + # want to package up the content of the docs-offline directory in a way + # that works around that. + { path = "docs-offline/**/*", format = "sdist" } ] [tool.poetry.scripts] @@ -54,7 +60,7 @@ mypy = "^1.0.0" pytest-cov = "^2.12.1" mkdocs = "^1.3.0" mkdocstrings = {extras = ["python"], version = "^0.20.0"} -mkdocs-material = "^8.2.15" +mkdocs-material = "^9.0.11" pre-commit = "^2.13.0" pytest-aiohttp = "^1.0.4" time-machine = "^2.6.0" @@ -63,6 +69,7 @@ syrupy = "^3.0.0" mkdocs-rss-plugin = "^1.5.0" httpx = "^0.23.1" msgpack-types = "^0.2.0" +types-setuptools = "^67.2.0.1" [tool.black] includes = "src" diff --git a/src/textual/_border.py b/src/textual/_border.py index 2da79b0ee..1a0c10ea0 100644 --- a/src/textual/_border.py +++ b/src/textual/_border.py @@ -191,7 +191,7 @@ BORDER_LOCATIONS: dict[ INVISIBLE_EDGE_TYPES = cast("frozenset[EdgeType]", frozenset(("", "none", "hidden"))) -BorderValue: TypeAlias = Tuple[EdgeType, Union[str, Color, Style]] +BorderValue: TypeAlias = Tuple[EdgeType, Color] BoxSegments: TypeAlias = Tuple[ Tuple[Segment, Segment, Segment], diff --git a/src/textual/_doc.py b/src/textual/_doc.py index b4921ed70..3b4c9d08b 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -99,7 +99,10 @@ def take_svg_screenshot( async def auto_pilot(pilot: Pilot) -> None: app = pilot.app await pilot.press(*press) + await pilot.wait_for_scheduled_animations() + await pilot.pause() svg = app.export_screenshot(title=title) + app.exit(svg) svg = app.run( diff --git a/src/textual/_text_backend.py b/src/textual/_text_backend.py deleted file mode 100644 index c707b19b2..000000000 --- a/src/textual/_text_backend.py +++ /dev/null @@ -1,159 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass -class TextEditorBackend: - """Represents a text editor (some text and a cursor)""" - - content: str = "" - cursor_index: int = 0 - - def set_content(self, text: str) -> None: - """Set the content of the editor - - Args: - text: The text to set as the content - """ - self.content = text - - def delete_back(self) -> bool: - """Delete the character behind the cursor and moves cursor back. If the - cursor is at the start of the content, does nothing other than immediately - return False. - - Returns: - True if the text content was modified. False otherwise. - """ - if self.cursor_index == 0: - return False - - new_text = ( - self.content[: self.cursor_index - 1] + self.content[self.cursor_index :] - ) - self.content = new_text - self.cursor_index = max(0, self.cursor_index - 1) - return True - - def delete_forward(self) -> bool: - """Delete the character in front of the cursor without moving the cursor. - - Returns: - True if the text content was modified. False otherwise. - """ - if self.cursor_index == len(self.content): - return False - - new_text = ( - self.content[: self.cursor_index] + self.content[self.cursor_index + 1 :] - ) - self.content = new_text - return True - - def cursor_left(self) -> bool: - """Move the cursor 1 character left in the text. Is a noop if cursor is at start. - - Returns: - True if the cursor moved. False otherwise. - """ - previous_index = self.cursor_index - new_index = max(0, previous_index - 1) - self.cursor_index = new_index - return previous_index != new_index - - def cursor_right(self) -> bool: - """Move the cursor 1 character right in the text. Is a noop if the cursor is at end. - - Returns: - True if the cursor moved. False otherwise. - """ - previous_index = self.cursor_index - new_index = min(len(self.content), previous_index + 1) - self.cursor_index = new_index - return previous_index != new_index - - def query_cursor_left(self) -> bool: - """Check if the cursor can move 1 codepoint left in the text. - - Returns: - True if the cursor can move left. False otherwise. - """ - previous_index = self.cursor_index - new_index = max(0, previous_index - 1) - return previous_index != new_index - - def query_cursor_right(self) -> str | None: - """Check if the cursor can move right (we can't move right if we're at the end) - and return the codepoint to the right of the cursor if it exists. If it doesn't - exist (e.g. we're at the end), then return None - - Returns: - The codepoint to the right of the cursor if it exists, otherwise None. - """ - previous_index = self.cursor_index - new_index = min(len(self.content), previous_index + 1) - if new_index == len(self.content): - return None - elif previous_index != new_index: - return self.content[new_index] - return None - - def cursor_text_start(self) -> bool: - """Move the cursor to the start of the text - - Returns: - True if the cursor moved. False otherwise. - """ - if self.cursor_index == 0: - return False - - self.cursor_index = 0 - return True - - def cursor_text_end(self) -> bool: - """Move the cursor to the end of the text - - Returns: - True if the cursor moved. False otherwise. - """ - text_length = len(self.content) - if self.cursor_index == text_length: - return False - - self.cursor_index = text_length - return True - - def insert(self, text: str) -> bool: - """Insert some text at the cursor position, and move the cursor - to the end of the newly inserted text. - - Args: - text: The text to insert - - Returns: - Always returns True since text should be insertable regardless of cursor location - """ - new_text = ( - self.content[: self.cursor_index] + text + self.content[self.cursor_index :] - ) - self.content = new_text - self.cursor_index = min(len(self.content), self.cursor_index + len(text)) - return True - - def get_range(self, start: int, end: int) -> str: - """Return the text between 2 indices. Useful for previews/views into - a subset of the content e.g. scrollable single-line input fields - - Args: - start: The starting index to return text from (inclusive) - end: The index to return text up to (exclusive) - - Returns: - The sliced string between start and end. - """ - return self.content[start:end] - - @property - def cursor_at_end(self): - return self.cursor_index == len(self.content) diff --git a/src/textual/_wait.py b/src/textual/_wait.py index 92fe8f761..a3c574422 100644 --- a/src/textual/_wait.py +++ b/src/textual/_wait.py @@ -2,7 +2,7 @@ from asyncio import sleep from time import monotonic, process_time SLEEP_GRANULARITY: float = 1 / 50 -SLEEP_IDLE: float = SLEEP_GRANULARITY / 2.0 +SLEEP_IDLE: float = SLEEP_GRANULARITY / 10.0 async def wait_for_idle( diff --git a/src/textual/app.py b/src/textual/app.py index b50d63e65..b52301a23 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -209,7 +209,11 @@ class _WriterThread(threading.Thread): self.join() -CSSPathType = Union[str, PurePath, List[Union[str, PurePath]], None] +CSSPathType = Union[ + str, + PurePath, + List[Union[str, PurePath]], +] CallThreadReturnType = TypeVar("CallThreadReturnType") @@ -241,7 +245,7 @@ class App(Generic[ReturnType], DOMNode): SCREENS: dict[str, Screen | Callable[[], Screen]] = {} _BASE_PATH: str | None = None - CSS_PATH: CSSPathType = None + CSS_PATH: CSSPathType | None = None TITLE: str | None = None """str | None: The default title for the application. @@ -270,7 +274,7 @@ class App(Generic[ReturnType], DOMNode): def __init__( self, driver_class: Type[Driver] | None = None, - css_path: CSSPathType = None, + css_path: CSSPathType | None = None, watch_css: bool = False, ): # N.B. This must be done *before* we call the parent constructor, because MessagePump's @@ -348,6 +352,7 @@ class App(Generic[ReturnType], DOMNode): css_path = css_path or self.CSS_PATH if css_path is not None: # When value(s) are supplied for CSS_PATH, we normalise them to a list of Paths. + css_paths: List[PurePath] if isinstance(css_path, str): css_paths = [Path(css_path)] elif isinstance(css_path, PurePath): @@ -355,7 +360,9 @@ class App(Generic[ReturnType], DOMNode): elif isinstance(css_path, list): css_paths = [] for path in css_path: - css_paths.append(Path(path) if isinstance(path, str) else path) + css_paths.append( + Path(path) if isinstance(path, str) else path, + ) else: raise CssPathError( "Expected a str, Path or list[str | Path] for the CSS_PATH." @@ -657,7 +664,7 @@ class App(Generic[ReturnType], DOMNode): Like asyncio apps in general, Textual apps are not thread-safe. If you call methods or set attributes on Textual objects from a thread, you may get unpredictable results. - This method will ensure that your code is ran within the correct context. + This method will ensure that your code runs within the correct context. Args: callback: A callable to run. @@ -753,7 +760,7 @@ class App(Generic[ReturnType], DOMNode): svg_filename_stem = f"{self.title.lower()} {dt}" for reserved in ' <>:"/\\|?*.': svg_filename_stem = svg_filename_stem.replace(reserved, "_") - svg_filename = svg_filename_stem + ".svg" + svg_filename = svg_filename_stem + ".svg" else: svg_filename = filename svg_path = os.path.expanduser(os.path.join(path, svg_filename)) @@ -832,7 +839,7 @@ class App(Generic[ReturnType], DOMNode): driver.send_event(key_event) await wait_for_idle(0) - await app._animator.wait_for_idle() + await app._animator.wait_until_complete() await wait_for_idle(0) @asynccontextmanager @@ -1230,7 +1237,7 @@ class App(Generic[ReturnType], DOMNode): return screen def push_screen(self, screen: Screen | str) -> AwaitMount: - """Push a new screen on the screen stack. + """Push a new screen on the screen stack, making it the current screen. Args: screen: A Screen instance or the name of an installed screen. @@ -1261,6 +1268,8 @@ class App(Generic[ReturnType], DOMNode): def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount: """Install a screen. + Installing a screen prevents Textual from destroying it when it is no longer on the screen stack. + Args: screen: Screen to install. name: Unique name of screen or None to auto-generate. diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index f69d10f16..f892ac4b9 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -10,11 +10,12 @@ when setting and getting. from __future__ import annotations from operator import attrgetter -from typing import TYPE_CHECKING, Generic, Iterable, NamedTuple, TypeVar, cast +from typing import TYPE_CHECKING, Generic, Iterable, NamedTuple, Sequence, TypeVar, cast import rich.errors import rich.repr from rich.style import Style +from typing_extensions import TypeAlias from .._border import normalize_border_value from ..color import Color, ColorParseError @@ -51,7 +52,7 @@ if TYPE_CHECKING: from .types import AlignHorizontal, AlignVertical, DockEdge, EdgeType -BorderDefinition = ( +BorderDefinition: TypeAlias = ( "Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]" ) @@ -153,7 +154,7 @@ class ScalarProperty: Returns: The Scalar object or ``None`` if it's not set. """ - return obj.get_rule(self.name) + return cast("Scalar | None", obj.get_rule(self.name)) def __set__( self, obj: StylesBase, value: float | int | Scalar | str | None @@ -233,7 +234,7 @@ class ScalarListProperty: def __get__( self, obj: StylesBase, objtype: type[StylesBase] | None = None ) -> tuple[Scalar, ...] | None: - return obj.get_rule(self.name) + return cast("tuple[Scalar, ...]", obj.get_rule(self.name)) def __set__( self, obj: StylesBase, value: str | Iterable[str | float] | None @@ -289,7 +290,10 @@ class BoxProperty: A ``tuple[EdgeType, Style]`` containing the string type of the box and it's style. Example types are "rounded", "solid", and "dashed". """ - return obj.get_rule(self.name) or ("", self._default_color) + return cast( + "tuple[EdgeType, Color]", + obj.get_rule(self.name) or ("", self._default_color), + ) def __set__(self, obj: Styles, border: tuple[EdgeType, str | Color] | None): """Set the box property. @@ -452,7 +456,7 @@ class BorderProperty: check_refresh() return if isinstance(border, tuple) and len(border) == 2: - _border = normalize_border_value(border) + _border = normalize_border_value(border) # type: ignore setattr(obj, top, _border) setattr(obj, right, _border) setattr(obj, bottom, _border) @@ -462,15 +466,15 @@ class BorderProperty: count = len(border) if count == 1: - _border = normalize_border_value(border[0]) + _border = normalize_border_value(border[0]) # type: ignore setattr(obj, top, _border) setattr(obj, right, _border) setattr(obj, bottom, _border) setattr(obj, left, _border) elif count == 2: _border1, _border2 = ( - normalize_border_value(border[0]), - normalize_border_value(border[1]), + normalize_border_value(border[0]), # type: ignore + normalize_border_value(border[1]), # type: ignore ) setattr(obj, top, _border1) setattr(obj, bottom, _border1) @@ -478,10 +482,10 @@ class BorderProperty: setattr(obj, left, _border2) elif count == 4: _border1, _border2, _border3, _border4 = ( - normalize_border_value(border[0]), - normalize_border_value(border[1]), - normalize_border_value(border[2]), - normalize_border_value(border[3]), + normalize_border_value(border[0]), # type: ignore + normalize_border_value(border[1]), # type: ignore + normalize_border_value(border[2]), # type: ignore + normalize_border_value(border[3]), # type: ignore ) setattr(obj, top, _border1) setattr(obj, right, _border2) @@ -513,7 +517,7 @@ class SpacingProperty: Returns: The Spacing. If unset, returns the null spacing ``(0, 0, 0, 0)``. """ - return obj.get_rule(self.name, NULL_SPACING) + return cast(Spacing, obj.get_rule(self.name, NULL_SPACING)) def __set__(self, obj: StylesBase, spacing: SpacingDimensions | None): """Set the Spacing. @@ -594,7 +598,7 @@ class LayoutProperty: Returns: The ``Layout`` object. """ - return obj.get_rule(self.name) + return cast("Layout | None", obj.get_rule(self.name)) def __set__(self, obj: StylesBase, layout: str | Layout | None): """ @@ -648,7 +652,7 @@ class OffsetProperty: The ``ScalarOffset`` indicating the adjustment that will be made to widget position prior to it being rendered. """ - return obj.get_rule(self.name, NULL_SCALAR) + return cast("ScalarOffset", obj.get_rule(self.name, NULL_SCALAR)) def __set__( self, obj: StylesBase, offset: tuple[int | str, int | str] | ScalarOffset | None @@ -734,7 +738,7 @@ class StringEnumProperty: Returns: The string property value. """ - return obj.get_rule(self.name, self._default) + return cast(str, obj.get_rule(self.name, self._default)) def _before_refresh(self, obj: StylesBase, value: str | None) -> None: """Do any housekeeping before asking for a layout refresh after a value change.""" @@ -795,7 +799,7 @@ class NameProperty: Returns: The name. """ - return obj.get_rule(self.name, "") + return cast(str, obj.get_rule(self.name, "")) def __set__(self, obj: StylesBase, name: str | None): """Set the name property. @@ -929,7 +933,7 @@ class StyleFlagsProperty: Returns: The ``Style`` object. """ - return obj.get_rule(self.name, Style.null()) + return cast(Style, obj.get_rule(self.name, Style.null())) def __set__(self, obj: StylesBase, style_flags: Style | str | None): """Set the style using a style flag string. @@ -992,7 +996,7 @@ class TransitionsProperty: e.g. ``{"offset": Transition(...), ...}``. If no transitions have been set, an empty ``dict`` is returned. """ - return obj.get_rule("transitions", {}) + return cast("dict[str, Transition]", obj.get_rule("transitions", {})) def __set__(self, obj: Styles, transitions: dict[str, Transition] | None) -> None: _rich_traceback_omit = True diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index ff66e9c84..26b67b07c 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -113,7 +113,6 @@ class StylesBuilder: suggested_property_name=suggested_property_name, ), ) - return tokens = declaration.tokens @@ -182,7 +181,13 @@ class StylesBuilder: """ if len(tokens) != 1: - string_enum_help_text(name, valid_values=list(valid_values), context="css"), + self.error( + name, + tokens[0], + string_enum_help_text( + name, valid_values=list(valid_values), context="css" + ), + ) token = tokens[0] token_name, value, _, _, location, _ = token @@ -239,7 +244,7 @@ class StylesBuilder: return if len(tokens) == 1: try: - self.styles._rules[name.replace("-", "_")] = Scalar.parse( + self.styles._rules[name.replace("-", "_")] = Scalar.parse( # type: ignore tokens[0].value ) except ScalarParseError: @@ -390,7 +395,7 @@ class StylesBuilder: name, num_values_supplied=len(space), context="css" ), ) - self.styles._rules[name] = Spacing.unpack(cast(SpacingDimensions, tuple(space))) + self.styles._rules[name] = Spacing.unpack(cast(SpacingDimensions, tuple(space))) # type: ignore def _process_space_partial(self, name: str, tokens: list[Token]) -> None: """Process granular margin / padding declarations.""" @@ -418,7 +423,7 @@ class StylesBuilder: spacing_list = list(current_spacing) spacing_list[_EDGE_SPACING_MAP[edge]] = space - self.styles._rules[style_name] = Spacing(*spacing_list) + self.styles._rules[style_name] = Spacing(*spacing_list) # type: ignore process_padding = _process_space process_margin = _process_space @@ -444,7 +449,7 @@ class StylesBuilder: token_name, value, _, _, _, _ = token if token_name == "token": if value in VALID_BORDER: - border_type = value + border_type = value # type: ignore else: try: border_color = Color.parse(value) @@ -464,7 +469,7 @@ class StylesBuilder: def _process_border_edge(self, edge: str, name: str, tokens: list[Token]) -> None: border = self._parse_border(name, tokens) - self.styles._rules[f"border_{edge}"] = border + self.styles._rules[f"border_{edge}"] = border # type: ignore def process_border(self, name: str, tokens: list[Token]) -> None: border = self._parse_border(name, tokens) @@ -486,7 +491,7 @@ class StylesBuilder: def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None: border = self._parse_border(name, tokens) - self.styles._rules[f"outline_{edge}"] = border + self.styles._rules[f"outline_{edge}"] = border # type: ignore def process_outline(self, name: str, tokens: list[Token]) -> None: border = self._parse_border(name, tokens) @@ -579,14 +584,14 @@ class StylesBuilder: color: Color | None = None alpha: float | None = None - self.styles._rules[f"auto_{name}"] = False + self.styles._rules[f"auto_{name}"] = False # type: ignore for token in tokens: if ( "background" not in name and token.name == "token" and token.value == "auto" ): - self.styles._rules[f"auto_{name}"] = True + self.styles._rules[f"auto_{name}"] = True # type: ignore elif token.name == "scalar": alpha_scalar = Scalar.parse(token.value) if alpha_scalar.unit != Unit.PERCENT: @@ -608,7 +613,7 @@ class StylesBuilder: if color is not None or alpha is not None: if alpha is not None: color = (color or Color(255, 255, 255)).with_alpha(alpha) - self.styles._rules[name] = color + self.styles._rules[name] = color # type: ignore process_tint = process_color process_background = process_color @@ -636,7 +641,7 @@ class StylesBuilder: ) style_definition = " ".join(token.value for token in tokens) - self.styles._rules[name.replace("-", "_")] = style_definition + self.styles._rules[name.replace("-", "_")] = style_definition # type: ignore process_link_style = process_text_style process_link_hover_style = process_text_style @@ -653,7 +658,7 @@ class StylesBuilder: text_align_help_text(), ) - self.styles._rules["text_align"] = tokens[0].value + self.styles._rules["text_align"] = tokens[0].value # type: ignore def process_dock(self, name: str, tokens: list[Token]) -> None: if not tokens: @@ -766,8 +771,8 @@ class StylesBuilder: align_error(name, token_horizontal) name = name.replace("-", "_") - self.styles._rules[f"{name}_horizontal"] = token_horizontal.value - self.styles._rules[f"{name}_vertical"] = token_vertical.value + self.styles._rules[f"{name}_horizontal"] = token_horizontal.value # type: ignore + self.styles._rules[f"{name}_vertical"] = token_vertical.value # type: ignore def process_align_horizontal(self, name: str, tokens: list[Token]) -> None: try: @@ -779,7 +784,7 @@ class StylesBuilder: string_enum_help_text(name, VALID_ALIGN_HORIZONTAL, context="css"), ) else: - self.styles._rules[name.replace("-", "_")] = value + self.styles._rules[name.replace("-", "_")] = value # type: ignore def process_align_vertical(self, name: str, tokens: list[Token]) -> None: try: @@ -791,7 +796,7 @@ class StylesBuilder: string_enum_help_text(name, VALID_ALIGN_VERTICAL, context="css"), ) else: - self.styles._rules[name.replace("-", "_")] = value + self.styles._rules[name.replace("-", "_")] = value # type: ignore process_content_align = process_align process_content_align_horizontal = process_align_horizontal @@ -807,7 +812,7 @@ class StylesBuilder: string_enum_help_text(name, VALID_SCROLLBAR_GUTTER, context="css"), ) else: - self.styles._rules[name.replace("-", "_")] = value + self.styles._rules[name.replace("-", "_")] = value # type: ignore def process_scrollbar_size(self, name: str, tokens: list[Token]) -> None: def scrollbar_size_error(name: str, token: Token) -> None: @@ -876,7 +881,7 @@ class StylesBuilder: token, table_rows_or_columns_help_text(name, token.value, context="css"), ) - self.styles._rules[name.replace("-", "_")] = scalars + self.styles._rules[name.replace("-", "_")] = scalars # type: ignore process_grid_rows = _process_grid_rows_or_columns process_grid_columns = _process_grid_rows_or_columns @@ -893,7 +898,7 @@ class StylesBuilder: value = int(token.value) if value == 0: self.error(name, token, integer_help_text(name)) - self.styles._rules[name.replace("-", "_")] = value + self.styles._rules[name.replace("-", "_")] = value # type: ignore process_grid_gutter_horizontal = _process_integer process_grid_gutter_vertical = _process_integer diff --git a/src/textual/css/model.py b/src/textual/css/model.py index cc68ee0af..3766606de 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Iterable import rich.repr +from ._help_renderables import HelpText from .styles import Styles from .tokenize import Token from .types import Specificity3 @@ -155,7 +156,7 @@ class SelectorSet: class RuleSet: selector_set: list[SelectorSet] = field(default_factory=list) styles: Styles = field(default_factory=Styles) - errors: list[tuple[Token, str]] = field(default_factory=list) + errors: list[tuple[Token, str | HelpText]] = field(default_factory=list) is_default_rules: bool = False tie_breaker: int = 0 diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py index fe23c084d..d1c5f709b 100644 --- a/src/textual/css/parse.py +++ b/src/textual/css/parse.py @@ -5,6 +5,7 @@ from pathlib import PurePath from typing import Iterable, Iterator, NoReturn from ..suggestions import get_suggestion +from ._help_renderables import HelpText from ._styles_builder import DeclarationError, StylesBuilder from .errors import UnresolvedVariableError from .model import ( @@ -130,7 +131,7 @@ def parse_rule_set( declaration = Declaration(token, "") - errors: list[tuple[Token, str]] = [] + errors: list[tuple[Token, str | HelpText]] = [] while True: token = next(tokens) @@ -182,7 +183,7 @@ def parse_declarations(css: str, path: str) -> Styles: styles_builder = StylesBuilder() declaration: Declaration | None = None - errors: list[tuple[Token, str]] = [] + errors: list[tuple[Token, str | HelpText]] = [] while True: token = next(tokens, None) diff --git a/src/textual/css/query.py b/src/textual/css/query.py index 7e5c149cc..428209fa4 100644 --- a/src/textual/css/query.py +++ b/src/textual/css/query.py @@ -255,7 +255,7 @@ class DOMQuery(Generic[QueryType]): # The IndexError was got, that's a good thing in this case. So # we return what we found. pass - return the_one + return cast("Widget", the_one) @overload def last(self) -> Widget: diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index ba2efa93c..b23bad368 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -367,8 +367,8 @@ class StylesBase(ABC): def auto_dimensions(self) -> bool: """Check if width or height are set to 'auto'.""" has_rule = self.has_rule - return (has_rule("width") and self.width.is_auto) or ( - has_rule("height") and self.height.is_auto + return (has_rule("width") and self.width.is_auto) or ( # type: ignore + has_rule("height") and self.height.is_auto # type: ignore ) @abstractmethod @@ -474,7 +474,7 @@ class StylesBase(ABC): @classmethod @lru_cache(maxsize=1024) - def parse(cls, css: str, path: str, *, node: DOMNode = None) -> Styles: + def parse(cls, css: str, path: str, *, node: DOMNode | None = None) -> Styles: """Parse CSS and return a Styles object. Args: @@ -603,7 +603,7 @@ class Styles(StylesBase): Returns: ``True`` if a rule was cleared, or ``False`` if it was already not set. """ - changed = self._rules.pop(rule, None) is not None + changed = self._rules.pop(rule, None) is not None # type: ignore if changed: self._updates += 1 return changed @@ -622,12 +622,12 @@ class Styles(StylesBase): ``True`` if the rule changed, otherwise ``False``. """ if value is None: - changed = self._rules.pop(rule, None) is not None + changed = self._rules.pop(rule, None) is not None # type: ignore if changed: self._updates += 1 return changed current = self._rules.get(rule) - self._rules[rule] = value + self._rules[rule] = value # type: ignore changed = current != value if changed: self._updates += 1 @@ -646,7 +646,7 @@ class Styles(StylesBase): def reset(self) -> None: """Reset the rules to initial state.""" self._updates += 1 - self._rules.clear() + self._rules.clear() # type: ignore def merge(self, other: Styles) -> None: """Merge values from another Styles. @@ -736,25 +736,25 @@ class Styles(StylesBase): left = get_rule(f"{name}_left") if top == right and right == bottom and bottom == left: - border_type, border_color = rules[f"{name}_top"] + border_type, border_color = rules[f"{name}_top"] # type: ignore yield name, f"{border_type} {border_color.hex}" return # Check for edges if has_top: - border_type, border_color = rules[f"{name}_top"] + border_type, border_color = rules[f"{name}_top"] # type: ignore yield f"{name}-top", f"{border_type} {border_color.hex}" if has_right: - border_type, border_color = rules[f"{name}_right"] + border_type, border_color = rules[f"{name}_right"] # type: ignore yield f"{name}-right", f"{border_type} {border_color.hex}" if has_bottom: - border_type, border_color = rules[f"{name}_bottom"] + border_type, border_color = rules[f"{name}_bottom"] # type: ignore yield f"{name}-bottom", f"{border_type} {border_color.hex}" if has_left: - border_type, border_color = rules[f"{name}_left"] + border_type, border_color = rules[f"{name}_left"] # type: ignore yield f"{name}-left", f"{border_type} {border_color.hex}" @property @@ -770,15 +770,14 @@ class Styles(StylesBase): rules = self.get_rules() get_rule = rules.get - has_rule = rules.__contains__ - if has_rule("display"): + if "display" in rules: append_declaration("display", rules["display"]) - if has_rule("visibility"): + if "visibility" in rules: append_declaration("visibility", rules["visibility"]) - if has_rule("padding"): + if "padding" in rules: append_declaration("padding", rules["padding"].css) - if has_rule("margin"): + if "margin" in rules: append_declaration("margin", rules["margin"].css) for name, rule in self._get_border_css_lines(rules, "border"): @@ -787,90 +786,90 @@ class Styles(StylesBase): for name, rule in self._get_border_css_lines(rules, "outline"): append_declaration(name, rule) - if has_rule("offset"): + if "offset" in rules: x, y = self.offset append_declaration("offset", f"{x} {y}") - if has_rule("dock"): + if "dock" in rules: append_declaration("dock", rules["dock"]) - if has_rule("layers"): + if "layers" in rules: append_declaration("layers", " ".join(self.layers)) - if has_rule("layer"): + if "layer" in rules: append_declaration("layer", self.layer) - if has_rule("layout"): + if "layout" in rules: assert self.layout is not None append_declaration("layout", self.layout.name) - if has_rule("color"): + if "color" in rules: append_declaration("color", self.color.hex) - if has_rule("background"): + if "background" in rules: append_declaration("background", self.background.hex) - if has_rule("text_style"): + if "text_style" in rules: append_declaration("text-style", str(get_rule("text_style"))) - if has_rule("tint"): + if "tint" in rules: append_declaration("tint", self.tint.css) - if has_rule("overflow_x"): + if "overflow_x" in rules: append_declaration("overflow-x", self.overflow_x) - if has_rule("overflow_y"): + if "overflow_y" in rules: append_declaration("overflow-y", self.overflow_y) - if has_rule("scrollbar_color"): + if "scrollbar_color" in rules: append_declaration("scrollbar-color", self.scrollbar_color.css) - if has_rule("scrollbar_color_hover"): + if "scrollbar_color_hover" in rules: append_declaration("scrollbar-color-hover", self.scrollbar_color_hover.css) - if has_rule("scrollbar_color_active"): + if "scrollbar_color_active" in rules: append_declaration( "scrollbar-color-active", self.scrollbar_color_active.css ) - if has_rule("scrollbar_corner_color"): + if "scrollbar_corner_color" in rules: append_declaration( "scrollbar-corner-color", self.scrollbar_corner_color.css ) - if has_rule("scrollbar_background"): + if "scrollbar_background" in rules: append_declaration("scrollbar-background", self.scrollbar_background.css) - if has_rule("scrollbar_background_hover"): + if "scrollbar_background_hover" in rules: append_declaration( "scrollbar-background-hover", self.scrollbar_background_hover.css ) - if has_rule("scrollbar_background_active"): + if "scrollbar_background_active" in rules: append_declaration( "scrollbar-background-active", self.scrollbar_background_active.css ) - if has_rule("scrollbar_gutter"): + if "scrollbar_gutter" in rules: append_declaration("scrollbar-gutter", self.scrollbar_gutter) - if has_rule("scrollbar_size"): + if "scrollbar_size" in rules: append_declaration( "scrollbar-size", f"{self.scrollbar_size_horizontal} {self.scrollbar_size_vertical}", ) else: - if has_rule("scrollbar_size_horizontal"): + if "scrollbar_size_horizontal" in rules: append_declaration( "scrollbar-size-horizontal", str(self.scrollbar_size_horizontal) ) - if has_rule("scrollbar_size_vertical"): + if "scrollbar_size_vertical" in rules: append_declaration( "scrollbar-size-vertical", str(self.scrollbar_size_vertical) ) - if has_rule("box_sizing"): + if "box_sizing" in rules: append_declaration("box-sizing", self.box_sizing) - if has_rule("width"): + if "width" in rules: append_declaration("width", str(self.width)) - if has_rule("height"): + if "height" in rules: append_declaration("height", str(self.height)) - if has_rule("min_width"): + if "min_width" in rules: append_declaration("min-width", str(self.min_width)) - if has_rule("min_height"): + if "min_height" in rules: append_declaration("min-height", str(self.min_height)) - if has_rule("max_width"): + if "max_width" in rules: append_declaration("max-width", str(self.min_width)) - if has_rule("max_height"): + if "max_height" in rules: append_declaration("max-height", str(self.min_height)) - if has_rule("transitions"): + if "transitions" in rules: append_declaration( "transition", ", ".join( @@ -879,74 +878,74 @@ class Styles(StylesBase): ), ) - if has_rule("align_horizontal") and has_rule("align_vertical"): + if "align_horizontal" in rules and "align_vertical" in rules: append_declaration( "align", f"{self.align_horizontal} {self.align_vertical}" ) - elif has_rule("align_horizontal"): + elif "align_horizontal" in rules: append_declaration("align-horizontal", self.align_horizontal) - elif has_rule("align_vertical"): + elif "align_vertical" in rules: append_declaration("align-vertical", self.align_vertical) - if has_rule("content_align_horizontal") and has_rule("content_align_vertical"): + if "content_align_horizontal" in rules and "content_align_vertical" in rules: append_declaration( "content-align", f"{self.content_align_horizontal} {self.content_align_vertical}", ) - elif has_rule("content_align_horizontal"): + elif "content_align_horizontal" in rules: append_declaration( "content-align-horizontal", self.content_align_horizontal ) - elif has_rule("content_align_vertical"): + elif "content_align_vertical" in rules: append_declaration("content-align-vertical", self.content_align_vertical) - if has_rule("text_align"): + if "text_align" in rules: append_declaration("text-align", self.text_align) - if has_rule("opacity"): + if "opacity" in rules: append_declaration("opacity", str(self.opacity)) - if has_rule("text_opacity"): + if "text_opacity" in rules: append_declaration("text-opacity", str(self.text_opacity)) - if has_rule("grid_columns"): + if "grid_columns" in rules: append_declaration( "grid-columns", " ".join(str(scalar) for scalar in self.grid_columns or ()), ) - if has_rule("grid_rows"): + if "grid_rows" in rules: append_declaration( "grid-rows", " ".join(str(scalar) for scalar in self.grid_rows or ()), ) - if has_rule("grid_size_columns"): + if "grid_size_columns" in rules: append_declaration("grid-size-columns", str(self.grid_size_columns)) - if has_rule("grid_size_rows"): + if "grid_size_rows" in rules: append_declaration("grid-size-rows", str(self.grid_size_rows)) - if has_rule("grid_gutter_horizontal"): + if "grid_gutter_horizontal" in rules: append_declaration( "grid-gutter-horizontal", str(self.grid_gutter_horizontal) ) - if has_rule("grid_gutter_vertical"): + if "grid_gutter_vertical" in rules: append_declaration("grid-gutter-vertical", str(self.grid_gutter_vertical)) - if has_rule("row_span"): + if "row_span" in rules: append_declaration("row-span", str(self.row_span)) - if has_rule("column_span"): + if "column_span" in rules: append_declaration("column-span", str(self.column_span)) - if has_rule("link_color"): + if "link_color" in rules: append_declaration("link-color", self.link_color.css) - if has_rule("link_background"): + if "link_background" in rules: append_declaration("link-background", self.link_background.css) - if has_rule("link_style"): + if "link_style" in rules: append_declaration("link-style", str(self.link_style)) - if has_rule("link_hover_color"): + if "link_hover_color" in rules: append_declaration("link-hover-color", self.link_hover_color.css) - if has_rule("link_hover_background"): + if "link_hover_background" in rules: append_declaration("link-hover-background", self.link_hover_background.css) - if has_rule("link_hover_style"): + if "link_hover_style" in rules: append_declaration("link-hover-style", str(self.link_hover_style)) lines.sort() diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index 0b676776d..975326202 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -4,7 +4,7 @@ import os from collections import defaultdict from operator import itemgetter from pathlib import Path, PurePath -from typing import Iterable, NamedTuple, cast +from typing import Iterable, NamedTuple, Sequence, cast import rich.repr from rich.console import Console, ConsoleOptions, RenderableType, RenderResult @@ -248,7 +248,7 @@ class Stylesheet: self.source[str(path)] = CssSource(css, False, 0) self._require_parse = True - def read_all(self, paths: list[PurePath]) -> None: + def read_all(self, paths: Sequence[PurePath]) -> None: """Read multiple CSS files, in order. Args: diff --git a/src/textual/dom.py b/src/textual/dom.py index 07f994958..1ed204aa9 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -111,7 +111,7 @@ class DOMNode(MessagePump): _css_type_names: ClassVar[frozenset[str]] = frozenset() # Generated list of bindings - _merged_bindings: ClassVar[Bindings] | None = None + _merged_bindings: ClassVar[Bindings | None] = None _reactives: ClassVar[dict[str, Reactive]] @@ -132,10 +132,12 @@ class DOMNode(MessagePump): check_identifiers("class name", *_classes) self._classes.update(_classes) - self.children = NodeList() + self.children: NodeList = NodeList() self._css_styles: Styles = Styles(self) self._inline_styles: Styles = Styles(self) - self.styles = RenderStyles(self, self._css_styles, self._inline_styles) + self.styles: RenderStyles = RenderStyles( + self, self._css_styles, self._inline_styles + ) # A mapping of class names to Styles set in COMPONENT_CLASSES self._component_styles: dict[str, RenderStyles] = {} diff --git a/src/textual/drivers/win32.py b/src/textual/drivers/win32.py index 34edc9930..30a335bd9 100644 --- a/src/textual/drivers/win32.py +++ b/src/textual/drivers/win32.py @@ -7,12 +7,12 @@ from ctypes import Structure, Union, byref, wintypes from ctypes.wintypes import BOOL, CHAR, DWORD, HANDLE, SHORT, UINT, WCHAR, WORD from typing import IO, Callable, List, Optional -from .._types import EventTarget +from .._types import MessageTarget from .._xterm_parser import XTermParser from ..events import Event, Resize from ..geometry import Size -KERNEL32 = ctypes.WinDLL("kernel32", use_last_error=True) +KERNEL32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore # Console input modes ENABLE_ECHO_INPUT = 0x0004 @@ -130,7 +130,7 @@ def _set_console_mode(file: IO, mode: int) -> bool: Returns: True on success, otherwise False. """ - windows_filehandle = msvcrt.get_osfhandle(file.fileno()) + windows_filehandle = msvcrt.get_osfhandle(file.fileno()) # type: ignore success = KERNEL32.SetConsoleMode(windows_filehandle, mode) return success @@ -144,7 +144,7 @@ def _get_console_mode(file: IO) -> int: Returns: The current console mode. """ - windows_filehandle = msvcrt.get_osfhandle(file.fileno()) + windows_filehandle = msvcrt.get_osfhandle(file.fileno()) # type: ignore mode = wintypes.DWORD() KERNEL32.GetConsoleMode(windows_filehandle, ctypes.byref(mode)) return mode.value @@ -211,7 +211,7 @@ class EventMonitor(threading.Thread): self, loop: AbstractEventLoop, app, - target: EventTarget, + target: MessageTarget, exit_event: threading.Event, process_event: Callable[[Event], None], ) -> None: diff --git a/src/textual/file_monitor.py b/src/textual/file_monitor.py index 778884865..a50625566 100644 --- a/src/textual/file_monitor.py +++ b/src/textual/file_monitor.py @@ -2,7 +2,7 @@ from __future__ import annotations import os from pathlib import PurePath -from typing import Callable +from typing import Callable, Sequence import rich.repr @@ -13,7 +13,7 @@ from ._callback import invoke class FileMonitor: """Monitors files for changes and invokes a callback when it does.""" - def __init__(self, paths: list[PurePath], callback: Callable) -> None: + def __init__(self, paths: Sequence[PurePath], callback: Callable) -> None: self.paths = paths self.callback = callback self._modified = self._get_last_modified_time() diff --git a/src/textual/keys.py b/src/textual/keys.py index b4253c3ae..81e0aca38 100644 --- a/src/textual/keys.py +++ b/src/textual/keys.py @@ -229,6 +229,7 @@ KEY_DISPLAY_ALIASES = { "escape": "ESC", "enter": "⏎", "minus": "-", + "space": "SPACE", } diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 5f4ca11b1..02362e5e1 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -53,7 +53,7 @@ class Pilot(Generic[ReturnType]): async def wait_for_scheduled_animations(self) -> None: """Wait for any current and scheduled animations to complete.""" await self._app.animator.wait_until_complete() - await wait_for_idle(0) + await wait_for_idle() async def exit(self, result: ReturnType) -> None: """Exit the app with the given result. diff --git a/src/textual/screen.py b/src/textual/screen.py index 86ea1c4b9..d1354a106 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -63,7 +63,7 @@ class Screen(Widget): @property def is_current(self) -> bool: - """Check if this screen is current (i.e. visible to user).""" + """Is the screen current (i.e. visible to user)?""" from .app import ScreenStackError try: @@ -231,7 +231,7 @@ class Screen(Widget): else: # Only move the focus if we are currently showing the focus if direction: - to_focus: Widget | None = None + to_focus = None chain_length = len(focus_chain) for step in range(1, len(focus_chain) + 1): node = focus_chain[ diff --git a/src/textual/scroll_view.py b/src/textual/scroll_view.py index e50bfcbfd..53c6c238a 100644 --- a/src/textual/scroll_view.py +++ b/src/textual/scroll_view.py @@ -30,14 +30,14 @@ class ScrollView(Widget): """Not transparent, i.e. renders something.""" return False - def watch_scroll_x(self, new_value: float) -> None: - if self.show_horizontal_scrollbar: - self.horizontal_scrollbar.position = int(new_value) + def watch_scroll_x(self, old_value: float, new_value: float) -> None: + if self.show_horizontal_scrollbar and round(old_value) != round(new_value): + self.horizontal_scrollbar.position = round(new_value) self.refresh() - def watch_scroll_y(self, new_value: float) -> None: - if self.show_vertical_scrollbar: - self.vertical_scrollbar.position = int(new_value) + def watch_scroll_y(self, old_value: float, new_value: float) -> None: + if self.show_vertical_scrollbar and round(old_value) != round(new_value): + self.vertical_scrollbar.position = round(new_value) self.refresh() def on_mount(self): diff --git a/src/textual/widget.py b/src/textual/widget.py index 4cb9fbbc5..197eb6147 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -117,7 +117,7 @@ class _Styled: """ def __init__( - self, renderable: "RenderableType", style: Style, link_style: Style | None + self, renderable: "ConsoleRenderable", style: Style, link_style: Style | None ) -> None: self.renderable = renderable self.style = style @@ -133,7 +133,7 @@ class _Styled: if style: apply = style.__add__ result_segments = ( - _Segment(text, apply(_style), control) + _Segment(text, apply(_style), None) for text, _style, control in result_segments ) link_style = self.link_style @@ -141,19 +141,22 @@ class _Styled: result_segments = ( _Segment( text, - style - if style._meta is None - else (style + link_style if "@click" in style.meta else style), + ( + style + if style._meta is None + else (style + link_style if "@click" in style.meta else style) + ), control, ) for text, style, control in result_segments + if style is not None ) return result_segments def __rich_measure__( self, console: "Console", options: "ConsoleOptions" ) -> Measurement: - return self.renderable.__rich_measure__(console, options) + return Measurement.get(console, options, self.renderable) class RenderCache(NamedTuple): @@ -1414,6 +1417,7 @@ class Widget(DOMNode): easing = DEFAULT_SCROLL_EASING if maybe_scroll_x: + assert x is not None self.scroll_target_x = x if x != self.scroll_x: self.animate( @@ -1425,6 +1429,7 @@ class Widget(DOMNode): ) scrolled_x = True if maybe_scroll_y: + assert y is not None self.scroll_target_y = y if y != self.scroll_y: self.animate( @@ -1438,10 +1443,12 @@ class Widget(DOMNode): else: if maybe_scroll_x: + assert x is not None scroll_x = self.scroll_x self.scroll_target_x = self.scroll_x = x scrolled_x = scroll_x != self.scroll_x if maybe_scroll_y: + assert y is not None scroll_y = self.scroll_y self.scroll_target_y = self.scroll_y = y scrolled_y = scroll_y != self.scroll_y diff --git a/src/textual/widgets/_switch.py b/src/textual/widgets/_switch.py index baaa44598..45e856dd1 100644 --- a/src/textual/widgets/_switch.py +++ b/src/textual/widgets/_switch.py @@ -31,12 +31,12 @@ class Switch(Widget, can_focus=True): """ COMPONENT_CLASSES: ClassVar[set[str]] = { - "switch--switch", + "switch--slider", } """ | Class | Description | | :- | :- | - | `switch--switch` | Targets the switch of the switch. | + | `switch--slider` | Targets the slider of the switch. | """ DEFAULT_CSS = """ @@ -48,7 +48,7 @@ class Switch(Widget, can_focus=True): padding: 0 2; } - Switch > .switch--switch { + Switch > .switch--slider { background: $panel-darken-2; color: $panel-lighten-2; } @@ -65,7 +65,7 @@ class Switch(Widget, can_focus=True): } - Switch.-on > .switch--switch { + Switch.-on > .switch--slider { color: $success; } """ @@ -128,7 +128,7 @@ class Switch(Widget, can_focus=True): self.set_class(slider_pos == 1, "-on") def render(self) -> RenderableType: - style = self.get_component_rich_style("switch--switch") + style = self.get_component_rich_style("switch--slider") return ScrollBarRender( virtual_size=100, window_size=50, @@ -144,12 +144,17 @@ class Switch(Widget, can_focus=True): return 1 def on_click(self) -> None: + """Toggle the state of the switch.""" self.toggle() def action_toggle(self) -> None: + """Toggle the state of the switch.""" self.toggle() def toggle(self) -> None: - """Toggle the switch value. As a result of the value changing, - a Switch.Changed message will be posted.""" + """Toggle the switch value. + + As a result of the value changing, a `Switch.Changed` message will + be posted. + """ self.value = not self.value diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 9421d47ad..3786b080d 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, ClassVar, Generic, NewType, TypeVar +from typing import TYPE_CHECKING, ClassVar, Generic, Iterable, NewType, TypeVar, cast import rich.repr from rich.style import NULL_STYLE, Style @@ -783,7 +783,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): assert self._tree_lines_cached is not None return self._tree_lines_cached - def _on_idle(self) -> None: + async def _on_idle(self, event: events.Idle) -> None: """Check tree needs a rebuild on idle.""" # Property calls build if required self._tree_lines @@ -891,6 +891,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): Returns: Strings for space, vertical, terminator and cross. """ + lines: tuple[Iterable[str], Iterable[str], Iterable[str], Iterable[str]] if self.show_guides: lines = self.LINES["default"] if style.bold: @@ -901,11 +902,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): lines = (" ", " ", " ", " ") guide_depth = max(0, self.guide_depth - 2) - lines = tuple( - f"{vertical}{horizontal * guide_depth} " - for vertical, horizontal in lines + guide_lines = tuple( + f"{characters[0]}{characters[1] * guide_depth} " + for characters in lines ) - return lines + return cast("tuple[str, str, str, str]", guide_lines) if is_hover: line_style = self.get_component_rich_style("tree--highlight-line") diff --git a/tests/test_text_backend.py b/tests/test_text_backend.py deleted file mode 100644 index 135931e3f..000000000 --- a/tests/test_text_backend.py +++ /dev/null @@ -1,165 +0,0 @@ -from textual._text_backend import TextEditorBackend - -CONTENT = "Hello, world!" - - -def test_set_content(): - editor = TextEditorBackend() - editor.set_content(CONTENT) - assert editor.content == CONTENT - - -def test_delete_back_cursor_at_start_is_noop(): - editor = TextEditorBackend(CONTENT) - assert not editor.delete_back() - assert editor == TextEditorBackend(CONTENT, 0) - - -def test_delete_back_cursor_at_end(): - editor = TextEditorBackend(CONTENT) - assert editor.cursor_text_end() - assert editor.delete_back() - assert editor == TextEditorBackend("Hello, world", 12) - - -def test_delete_back_cursor_in_middle(): - editor = TextEditorBackend(CONTENT, 5) - assert editor.delete_back() - assert editor == TextEditorBackend("Hell, world!", 4) - - -def test_delete_forward_cursor_at_start(): - editor = TextEditorBackend(CONTENT) - assert editor.delete_forward() - assert editor.content == "ello, world!" - - -def test_delete_forward_cursor_at_end_is_noop(): - editor = TextEditorBackend(CONTENT) - assert editor.cursor_text_end() - assert not editor.delete_forward() - assert editor == TextEditorBackend(CONTENT, len(CONTENT)) - - -def test_delete_forward_cursor_in_middle(): - editor = TextEditorBackend(CONTENT, 5) - editor.cursor_index = 5 - assert editor.delete_forward() - assert editor == TextEditorBackend("Hello world!", 5) - - -def test_cursor_left_cursor_at_start_is_noop(): - editor = TextEditorBackend(CONTENT) - assert not editor.cursor_left() - assert editor == TextEditorBackend(CONTENT) - - -def test_cursor_left_cursor_in_middle(): - editor = TextEditorBackend(CONTENT, 6) - assert editor.cursor_left() - assert editor == TextEditorBackend(CONTENT, 5) - - -def test_cursor_left_cursor_at_end(): - editor = TextEditorBackend(CONTENT, len(CONTENT)) - assert editor.cursor_left() - assert editor == TextEditorBackend(CONTENT, len(CONTENT) - 1) - - -def test_cursor_right_cursor_at_start(): - editor = TextEditorBackend(CONTENT) - assert editor.cursor_right() - assert editor == TextEditorBackend(CONTENT, 1) - - -def test_cursor_right_cursor_in_middle(): - editor = TextEditorBackend(CONTENT, 5) - assert editor.cursor_right() - assert editor == TextEditorBackend(CONTENT, 6) - - -def test_cursor_right_cursor_at_end_is_noop(): - editor = TextEditorBackend(CONTENT, len(CONTENT)) - editor.cursor_right() - assert editor == TextEditorBackend(CONTENT, len(CONTENT)) - - -def test_query_cursor_left_cursor_at_start_returns_false(): - editor = TextEditorBackend(CONTENT) - assert not editor.query_cursor_left() - - -def test_query_cursor_left_cursor_at_end_returns_true(): - editor = TextEditorBackend(CONTENT, len(CONTENT)) - assert editor.query_cursor_left() - - -def test_query_cursor_left_cursor_in_middle_returns_true(): - editor = TextEditorBackend(CONTENT, 6) - assert editor.query_cursor_left() - - -def test_query_cursor_right_cursor_at_start_returns_true(): - editor = TextEditorBackend(CONTENT) - assert editor.query_cursor_right() - - -def test_query_cursor_right_cursor_in_middle_returns_true(): - editor = TextEditorBackend(CONTENT, 6) - assert editor.query_cursor_right() - - -def test_query_cursor_right_cursor_at_end_returns_false(): - editor = TextEditorBackend(CONTENT, len(CONTENT)) - assert not editor.query_cursor_right() - - -def test_cursor_text_start_cursor_already_at_start(): - editor = TextEditorBackend(CONTENT) - assert not editor.cursor_text_start() - assert editor.cursor_index == 0 - - -def test_cursor_text_start_cursor_in_middle(): - editor = TextEditorBackend(CONTENT, 6) - assert editor.cursor_text_start() - assert editor.cursor_index == 0 - - -def test_cursor_text_end_cursor_already_at_end(): - editor = TextEditorBackend(CONTENT, len(CONTENT)) - assert not editor.cursor_text_end() - assert editor.cursor_index == len(CONTENT) - - -def test_cursor_text_end_cursor_in_middle(): - editor = TextEditorBackend(CONTENT, len(CONTENT)) - assert not editor.cursor_text_end() - assert editor.cursor_index == len(CONTENT) - - -def test_insert_at_cursor_cursor_at_start(): - editor = TextEditorBackend(CONTENT) - assert editor.insert("ABC") - assert editor.content == "ABC" + CONTENT - assert editor.cursor_index == len("ABC") - - -def test_insert_at_cursor_cursor_in_middle(): - start_cursor_index = 6 - editor = TextEditorBackend(CONTENT, start_cursor_index) - assert editor.insert("ABC") - assert editor.content == "Hello,ABC world!" - assert editor.cursor_index == start_cursor_index + len("ABC") - - -def test_insert_at_cursor_cursor_at_end(): - editor = TextEditorBackend(CONTENT, len(CONTENT)) - assert editor.insert("ABC") - assert editor.content == CONTENT + "ABC" - assert editor.cursor_index == len(editor.content) - - -def test_get_range(): - editor = TextEditorBackend(CONTENT) - assert editor.get_range(0, 5) == "Hello"