Merge branch 'main' into datatable-cell-keys

This commit is contained in:
darrenburns
2023-02-13 16:02:37 +00:00
committed by GitHub
40 changed files with 595 additions and 687 deletions

3
.gitignore vendored
View File

@@ -110,6 +110,9 @@ venv.bak/
# mkdocs documentation
/site
/docs-offline
/mkdocs-nav-online.yml
/mkdocs-nav-offline.yml
# mypy
.mypy_cache/

View File

@@ -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

View File

@@ -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

View File

@@ -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!
<details>
<summary> 🎬 Demonstration </summary>
@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

View File

@@ -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).
<!-- more -->
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.

View File

@@ -1,7 +1,7 @@
#dialog {
grid-size: 2;
grid-gutter: 1 2;
margin: 1 2;
padding: 1 2;
}
#question {

View File

@@ -22,7 +22,7 @@ Switch {
background: darkslategrey;
}
#custom-design > .switch--switch {
#custom-design > .switch--slider {
color: dodgerblue;
background: darkslateblue;
}

View File

@@ -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.

View File

@@ -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"}
```

View File

@@ -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"

100
mkdocs-common.yml Normal file
View File

@@ -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 &copy; Textualize, Inc

View File

@@ -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 &copy; Textualize, Inc

11
mkdocs-offline.yml Normal file
View File

@@ -0,0 +1,11 @@
INHERIT: mkdocs-common.yml
plugins:
offline:
privacy:
exclude:
glob:
- "**/_template.md"
- blog/*
site_dir: docs-offline

16
mkdocs-online.yml Normal file
View File

@@ -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

167
poetry.lock generated
View File

@@ -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"},
]

View File

@@ -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"

View File

@@ -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],

View File

@@ -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(

View File

@@ -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)

View File

@@ -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(

View File

@@ -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.
@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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()

View File

@@ -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:

View File

@@ -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] = {}

View File

@@ -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:

View File

@@ -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()

View File

@@ -229,6 +229,7 @@ KEY_DISPLAY_ALIASES = {
"escape": "ESC",
"enter": "",
"minus": "-",
"space": "SPACE",
}

View File

@@ -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.

View File

@@ -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[

View File

@@ -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):

View File

@@ -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),
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

View File

@@ -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

View File

@@ -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")

View File

@@ -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"