From 2ecf0411c05aa50ec37e4227df77c66b3831a72e Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 7 Apr 2025 14:56:14 +0100 Subject: [PATCH 01/14] apply pre-commit --- .github/ISSUE_TEMPLATE/task.md | 2 -- CODE_OF_CONDUCT.md | 2 +- docs/blog/images/faster-updates.excalidraw.svg | 2 +- docs/blog/index.md | 1 - docs/blog/posts/helo-world.md | 1 - docs/blog/posts/release0-6-0.md | 1 - docs/examples/app/event01.py | 2 +- docs/examples/app/question01.py | 2 +- docs/examples/app/question03.py | 2 +- docs/examples/guide/actions/actions01.py | 2 +- docs/examples/guide/dom2.py | 2 +- docs/examples/guide/styles/box_sizing01.py | 1 - docs/examples/guide/styles/dimensions01.py | 1 - docs/examples/guide/styles/dimensions02.py | 1 - docs/examples/guide/styles/dimensions03.py | 1 - docs/examples/guide/styles/dimensions04.py | 1 - docs/examples/guide/styles/margin01.py | 1 - docs/examples/guide/styles/outline01.py | 1 - docs/examples/guide/styles/padding01.py | 1 - docs/examples/guide/styles/padding02.py | 1 - docs/examples/guide/widgets/checker03.py | 6 +++--- docs/examples/guide/widgets/checker04.py | 8 ++++---- docs/images/actions/format.excalidraw.svg | 2 +- docs/images/animation/animation.excalidraw.svg | 2 +- docs/images/child_combinator.excalidraw.svg | 2 +- docs/images/css_stopwatch.excalidraw.svg | 2 +- docs/images/descendant_combinator.excalidraw.svg | 2 +- docs/images/dom1.excalidraw.svg | 2 +- docs/images/dom2.excalidraw.svg | 2 +- docs/images/dom3.excalidraw.svg | 2 +- docs/images/events/bubble1.excalidraw.svg | 2 +- docs/images/events/bubble2.excalidraw.svg | 2 +- docs/images/events/bubble3.excalidraw.svg | 2 +- docs/images/events/naming.excalidraw.svg | 2 +- docs/images/events/queue.excalidraw.svg | 2 +- docs/images/events/queue2.excalidraw.svg | 2 +- docs/images/input/coords.excalidraw.svg | 2 +- docs/images/layout/align.excalidraw.svg | 2 +- docs/images/layout/center.excalidraw.svg | 2 +- docs/images/layout/dock.excalidraw.svg | 2 +- docs/images/layout/grid.excalidraw.svg | 2 +- docs/images/layout/horizontal.excalidraw.svg | 2 +- docs/images/layout/offset.excalidraw.svg | 2 +- docs/images/layout/vertical.excalidraw.svg | 2 +- docs/images/screens/pop_screen.excalidraw.svg | 2 +- docs/images/screens/push_screen.excalidraw.svg | 2 +- docs/images/screens/switch_screen.excalidraw.svg | 2 +- docs/images/stopwatch.excalidraw.svg | 2 +- docs/images/styles/border_box.excalidraw.svg | 2 +- docs/images/styles/box.excalidraw.svg | 2 +- docs/images/styles/content_box.excalidraw.svg | 2 +- examples/json_tree.py | 2 +- src/textual/_tree_sitter.py | 2 +- src/textual/widgets/text_area.py | 2 +- tests/input/test_input_restrict.py | 1 - .../option_list/test_option_prompt_replacement.py | 15 +++++++++++---- tests/select/test_blank_and_clear.py | 1 + .../test_selection_click_checkbox.py | 6 ++++-- tests/test_expand_tabs.py | 7 ++++++- tests/test_unmount.py | 1 - tests/test_validation.py | 7 ++++++- tests/test_win_sleep.py | 2 +- tests/text_area/test_edit_via_api.py | 4 +--- tests/text_area/test_selection_bindings.py | 4 +--- tests/tree/test_node_refresh.py | 5 ++++- tools/widget_documentation.py | 1 + 66 files changed, 82 insertions(+), 78 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md index 9c4be4e926..f1c36916ed 100644 --- a/.github/ISSUE_TEMPLATE/task.md +++ b/.github/ISSUE_TEMPLATE/task.md @@ -6,5 +6,3 @@ labels: '' assignees: '' --- - - diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d1435248d5..e54b608481 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity). For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. \ No newline at end of file +https://www.contributor-covenant.org/translations. diff --git a/docs/blog/images/faster-updates.excalidraw.svg b/docs/blog/images/faster-updates.excalidraw.svg index 18340ee26a..47586a065c 100644 --- a/docs/blog/images/faster-updates.excalidraw.svg +++ b/docs/blog/images/faster-updates.excalidraw.svg @@ -13,4 +13,4 @@ } - UpdateWriteUpdateWriteUpdateWriteUpdateWriteBeforeAfterTime \ No newline at end of file + UpdateWriteUpdateWriteUpdateWriteUpdateWriteBeforeAfterTime diff --git a/docs/blog/index.md b/docs/blog/index.md index 210ffd3f9b..8b01728346 100644 --- a/docs/blog/index.md +++ b/docs/blog/index.md @@ -1,2 +1 @@ # Textual Blog - diff --git a/docs/blog/posts/helo-world.md b/docs/blog/posts/helo-world.md index 90dab804b6..fa90e1f5a0 100644 --- a/docs/blog/posts/helo-world.md +++ b/docs/blog/posts/helo-world.md @@ -16,4 +16,3 @@ Welcome to the first post on the Textual blog. I plan on using this as a place to make announcements regarding new releases of Textual, and any other relevant news. The first piece of news is that we've reorganized this site a little. The Events, Styles, and Widgets references are now under "Reference", and what used to be under "Reference" is now "API" which contains API-level documentation. I hope that's a little clearer than it used to be! - diff --git a/docs/blog/posts/release0-6-0.md b/docs/blog/posts/release0-6-0.md index 715b6a3c39..958b89f74b 100644 --- a/docs/blog/posts/release0-6-0.md +++ b/docs/blog/posts/release0-6-0.md @@ -103,4 +103,3 @@ As always, there are a number of fixes in this release. Mostly related to layout ## What's next? The next release will focus on *pain points* we discovered while in a dog-fooding phase (see the [DevLog](https://textual.textualize.io/blog/category/devlog/) for details on what Textual devs have been building). - diff --git a/docs/examples/app/event01.py b/docs/examples/app/event01.py index 0f3bc3cdca..cbe6290e9b 100644 --- a/docs/examples/app/event01.py +++ b/docs/examples/app/event01.py @@ -1,5 +1,5 @@ -from textual.app import App from textual import events +from textual.app import App class EventApp(App): diff --git a/docs/examples/app/question01.py b/docs/examples/app/question01.py index 04c4e86898..c7be464892 100644 --- a/docs/examples/app/question01.py +++ b/docs/examples/app/question01.py @@ -1,5 +1,5 @@ from textual.app import App, ComposeResult -from textual.widgets import Label, Button +from textual.widgets import Button, Label class QuestionApp(App[str]): diff --git a/docs/examples/app/question03.py b/docs/examples/app/question03.py index 6fc372dedb..411c4ae1ae 100644 --- a/docs/examples/app/question03.py +++ b/docs/examples/app/question03.py @@ -1,5 +1,5 @@ from textual.app import App, ComposeResult -from textual.widgets import Label, Button +from textual.widgets import Button, Label class QuestionApp(App[str]): diff --git a/docs/examples/guide/actions/actions01.py b/docs/examples/guide/actions/actions01.py index 54b366f95e..7b599854dd 100644 --- a/docs/examples/guide/actions/actions01.py +++ b/docs/examples/guide/actions/actions01.py @@ -1,5 +1,5 @@ -from textual.app import App from textual import events +from textual.app import App class ActionsApp(App): diff --git a/docs/examples/guide/dom2.py b/docs/examples/guide/dom2.py index 35b6703278..84d9ebe963 100644 --- a/docs/examples/guide/dom2.py +++ b/docs/examples/guide/dom2.py @@ -1,5 +1,5 @@ from textual.app import App, ComposeResult -from textual.widgets import Header, Footer +from textual.widgets import Footer, Header class ExampleApp(App): diff --git a/docs/examples/guide/styles/box_sizing01.py b/docs/examples/guide/styles/box_sizing01.py index f2799b6e3e..7c276a4994 100644 --- a/docs/examples/guide/styles/box_sizing01.py +++ b/docs/examples/guide/styles/box_sizing01.py @@ -1,7 +1,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. diff --git a/docs/examples/guide/styles/dimensions01.py b/docs/examples/guide/styles/dimensions01.py index ed479f0f34..021e0d343a 100644 --- a/docs/examples/guide/styles/dimensions01.py +++ b/docs/examples/guide/styles/dimensions01.py @@ -1,7 +1,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. diff --git a/docs/examples/guide/styles/dimensions02.py b/docs/examples/guide/styles/dimensions02.py index 339aade995..d263c404e6 100644 --- a/docs/examples/guide/styles/dimensions02.py +++ b/docs/examples/guide/styles/dimensions02.py @@ -1,7 +1,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. diff --git a/docs/examples/guide/styles/dimensions03.py b/docs/examples/guide/styles/dimensions03.py index 4d361227e4..1dc49e891f 100644 --- a/docs/examples/guide/styles/dimensions03.py +++ b/docs/examples/guide/styles/dimensions03.py @@ -1,7 +1,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. diff --git a/docs/examples/guide/styles/dimensions04.py b/docs/examples/guide/styles/dimensions04.py index 405b5545e9..c1f52ee02b 100644 --- a/docs/examples/guide/styles/dimensions04.py +++ b/docs/examples/guide/styles/dimensions04.py @@ -1,7 +1,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. diff --git a/docs/examples/guide/styles/margin01.py b/docs/examples/guide/styles/margin01.py index 7036cb7257..7c8f7eac80 100644 --- a/docs/examples/guide/styles/margin01.py +++ b/docs/examples/guide/styles/margin01.py @@ -1,7 +1,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. diff --git a/docs/examples/guide/styles/outline01.py b/docs/examples/guide/styles/outline01.py index cd77d0b8c6..ef67f3cba2 100644 --- a/docs/examples/guide/styles/outline01.py +++ b/docs/examples/guide/styles/outline01.py @@ -1,7 +1,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. diff --git a/docs/examples/guide/styles/padding01.py b/docs/examples/guide/styles/padding01.py index 92c68948aa..916d6825b9 100644 --- a/docs/examples/guide/styles/padding01.py +++ b/docs/examples/guide/styles/padding01.py @@ -1,7 +1,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. diff --git a/docs/examples/guide/styles/padding02.py b/docs/examples/guide/styles/padding02.py index 50bf0b940c..828f5cb54d 100644 --- a/docs/examples/guide/styles/padding02.py +++ b/docs/examples/guide/styles/padding02.py @@ -1,7 +1,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. diff --git a/docs/examples/guide/widgets/checker03.py b/docs/examples/guide/widgets/checker03.py index 03ca19381c..7b4a3d5b97 100644 --- a/docs/examples/guide/widgets/checker03.py +++ b/docs/examples/guide/widgets/checker03.py @@ -1,11 +1,11 @@ from __future__ import annotations +from rich.segment import Segment + from textual.app import App, ComposeResult from textual.geometry import Size -from textual.strip import Strip from textual.scroll_view import ScrollView - -from rich.segment import Segment +from textual.strip import Strip class CheckerBoard(ScrollView): diff --git a/docs/examples/guide/widgets/checker04.py b/docs/examples/guide/widgets/checker04.py index 0445ffea5a..fc91798d74 100644 --- a/docs/examples/guide/widgets/checker04.py +++ b/docs/examples/guide/widgets/checker04.py @@ -1,14 +1,14 @@ from __future__ import annotations +from rich.segment import Segment +from rich.style import Style + from textual import events from textual.app import App, ComposeResult from textual.geometry import Offset, Region, Size from textual.reactive import var -from textual.strip import Strip from textual.scroll_view import ScrollView - -from rich.segment import Segment -from rich.style import Style +from textual.strip import Strip class CheckerBoard(ScrollView): diff --git a/docs/images/actions/format.excalidraw.svg b/docs/images/actions/format.excalidraw.svg index b74fbad3fd..7a7bdd6925 100644 --- a/docs/images/actions/format.excalidraw.svg +++ b/docs/images/actions/format.excalidraw.svg @@ -13,4 +13,4 @@ } - Optional namespaceAction nameOptional parametersapp.set_background('red') \ No newline at end of file + Optional namespaceAction nameOptional parametersapp.set_background('red') diff --git a/docs/images/animation/animation.excalidraw.svg b/docs/images/animation/animation.excalidraw.svg index da9a23f259..022b7e1ce9 100644 --- a/docs/images/animation/animation.excalidraw.svg +++ b/docs/images/animation/animation.excalidraw.svg @@ -13,4 +13,4 @@ } - timevalue \ No newline at end of file + timevalue diff --git a/docs/images/child_combinator.excalidraw.svg b/docs/images/child_combinator.excalidraw.svg index 47e3b0d8e5..684dd91739 100644 --- a/docs/images/child_combinator.excalidraw.svg +++ b/docs/images/child_combinator.excalidraw.svg @@ -13,4 +13,4 @@ } - Container( id="dialog")Horizontal( classes="buttons")Button("Yes")Button("No")Screen()Container( id="sidebar")Button( "Install")Underline this button \ No newline at end of file + Container( id="dialog")Horizontal( classes="buttons")Button("Yes")Button("No")Screen()Container( id="sidebar")Button( "Install")Underline this button diff --git a/docs/images/css_stopwatch.excalidraw.svg b/docs/images/css_stopwatch.excalidraw.svg index 931702ccff..ed0e49edbe 100644 --- a/docs/images/css_stopwatch.excalidraw.svg +++ b/docs/images/css_stopwatch.excalidraw.svg @@ -13,4 +13,4 @@ } - Stop00:00:00.00ResetStart00:00:00.00StopwatchStarted Stopwatch \ No newline at end of file + Stop00:00:00.00ResetStart00:00:00.00StopwatchStarted Stopwatch diff --git a/docs/images/descendant_combinator.excalidraw.svg b/docs/images/descendant_combinator.excalidraw.svg index 1f5a7ce8d1..e4eba50a7f 100644 --- a/docs/images/descendant_combinator.excalidraw.svg +++ b/docs/images/descendant_combinator.excalidraw.svg @@ -13,4 +13,4 @@ } - Container( id="dialog")Horizontal( classes="buttons")Button("Yes")Button("No")Screen()Container( id="sidebar")Button( "Install")match these*don't* match this \ No newline at end of file + Container( id="dialog")Horizontal( classes="buttons")Button("Yes")Button("No")Screen()Container( id="sidebar")Button( "Install")match these*don't* match this diff --git a/docs/images/dom1.excalidraw.svg b/docs/images/dom1.excalidraw.svg index 54634dc1de..063fb10eea 100644 --- a/docs/images/dom1.excalidraw.svg +++ b/docs/images/dom1.excalidraw.svg @@ -13,4 +13,4 @@ } - ExampleApp()Screen() \ No newline at end of file + ExampleApp()Screen() diff --git a/docs/images/dom2.excalidraw.svg b/docs/images/dom2.excalidraw.svg index bd636267f4..62ffe4dbde 100644 --- a/docs/images/dom2.excalidraw.svg +++ b/docs/images/dom2.excalidraw.svg @@ -13,4 +13,4 @@ } - ExampleApp()Screen()Header()Footer() \ No newline at end of file + ExampleApp()Screen()Header()Footer() diff --git a/docs/images/dom3.excalidraw.svg b/docs/images/dom3.excalidraw.svg index 86fb5e78d8..6ff78afdb5 100644 --- a/docs/images/dom3.excalidraw.svg +++ b/docs/images/dom3.excalidraw.svg @@ -13,4 +13,4 @@ } - App()Screen()Header()Footer()Container( id="dialog")Horizontal( classes="buttons")Button( "Yes", variant="success")Button( "No", variant="error")Static( QUESTION, classes="questions") \ No newline at end of file + App()Screen()Header()Footer()Container( id="dialog")Horizontal( classes="buttons")Button( "Yes", variant="success")Button( "No", variant="error")Static( QUESTION, classes="questions") diff --git a/docs/images/events/bubble1.excalidraw.svg b/docs/images/events/bubble1.excalidraw.svg index f4b79d1011..3f201adfed 100644 --- a/docs/images/events/bubble1.excalidraw.svg +++ b/docs/images/events/bubble1.excalidraw.svg @@ -13,4 +13,4 @@ } - App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T") \ No newline at end of file + App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T") diff --git a/docs/images/events/bubble2.excalidraw.svg b/docs/images/events/bubble2.excalidraw.svg index e3e4d20791..f98666ad45 100644 --- a/docs/images/events/bubble2.excalidraw.svg +++ b/docs/images/events/bubble2.excalidraw.svg @@ -13,4 +13,4 @@ } - App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T")events.Key(key="T")bubble \ No newline at end of file + App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T")events.Key(key="T")bubble diff --git a/docs/images/events/bubble3.excalidraw.svg b/docs/images/events/bubble3.excalidraw.svg index afc97dc2f0..432a4f031d 100644 --- a/docs/images/events/bubble3.excalidraw.svg +++ b/docs/images/events/bubble3.excalidraw.svg @@ -13,4 +13,4 @@ } - App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T")events.Key(key="T")events.Key(key="T")bubble \ No newline at end of file + App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T")events.Key(key="T")events.Key(key="T")bubble diff --git a/docs/images/events/naming.excalidraw.svg b/docs/images/events/naming.excalidraw.svg index 5a9a2fb4ca..0a69c6bcce 100644 --- a/docs/images/events/naming.excalidraw.svg +++ b/docs/images/events/naming.excalidraw.svg @@ -13,4 +13,4 @@ } - Makes the methoda message handlerMessage namespace(outer class)Name ofmessage classon_color_button_selected \ No newline at end of file + Makes the methoda message handlerMessage namespace(outer class)Name ofmessage classon_color_button_selected diff --git a/docs/images/events/queue.excalidraw.svg b/docs/images/events/queue.excalidraw.svg index 45230f41ba..8747674c28 100644 --- a/docs/images/events/queue.excalidraw.svg +++ b/docs/images/events/queue.excalidraw.svg @@ -13,4 +13,4 @@ } - events.Key(key="T")events.Key(key="e")events.Key(key="x")Message queueon_key(event)Event handlerevents.Key(key="t") \ No newline at end of file + events.Key(key="T")events.Key(key="e")events.Key(key="x")Message queueon_key(event)Event handlerevents.Key(key="t") diff --git a/docs/images/events/queue2.excalidraw.svg b/docs/images/events/queue2.excalidraw.svg index d4ed6be006..b5fbb6dfde 100644 --- a/docs/images/events/queue2.excalidraw.svg +++ b/docs/images/events/queue2.excalidraw.svg @@ -13,4 +13,4 @@ } - events.Key(key="e")events.Key(key="x")events.Key(key="t")Tevents.Key(key="x")events.Key(key="t")Teevents.Key(key="t")TexText \ No newline at end of file + events.Key(key="e")events.Key(key="x")events.Key(key="t")Tevents.Key(key="x")events.Key(key="t")Teevents.Key(key="t")TexText diff --git a/docs/images/input/coords.excalidraw.svg b/docs/images/input/coords.excalidraw.svg index fb925e22c3..7a4114e20d 100644 --- a/docs/images/input/coords.excalidraw.svg +++ b/docs/images/input/coords.excalidraw.svg @@ -13,4 +13,4 @@ } - XyXy(0, 0)(0, 0)Widget \ No newline at end of file + XyXy(0, 0)(0, 0)Widget diff --git a/docs/images/layout/align.excalidraw.svg b/docs/images/layout/align.excalidraw.svg index fd2f8e94fb..2dd2ac4fc7 100644 --- a/docs/images/layout/align.excalidraw.svg +++ b/docs/images/layout/align.excalidraw.svg @@ -13,4 +13,4 @@ } - align-horizontal: leftalign-horizontal: centeralign-horizontal: right \ No newline at end of file + align-horizontal: leftalign-horizontal: centeralign-horizontal: right diff --git a/docs/images/layout/center.excalidraw.svg b/docs/images/layout/center.excalidraw.svg index b9e15e7a9b..0420f1aeaa 100644 --- a/docs/images/layout/center.excalidraw.svg +++ b/docs/images/layout/center.excalidraw.svg @@ -13,4 +13,4 @@ } - Widget \ No newline at end of file + Widget diff --git a/docs/images/layout/dock.excalidraw.svg b/docs/images/layout/dock.excalidraw.svg index fde040991a..ffded907a1 100644 --- a/docs/images/layout/dock.excalidraw.svg +++ b/docs/images/layout/dock.excalidraw.svg @@ -13,4 +13,4 @@ } - Docked widgetLayout widgets \ No newline at end of file + Docked widgetLayout widgets diff --git a/docs/images/layout/grid.excalidraw.svg b/docs/images/layout/grid.excalidraw.svg index aed2f9d24f..c295751e87 100644 --- a/docs/images/layout/grid.excalidraw.svg +++ b/docs/images/layout/grid.excalidraw.svg @@ -13,4 +13,4 @@ } - \ No newline at end of file + diff --git a/docs/images/layout/horizontal.excalidraw.svg b/docs/images/layout/horizontal.excalidraw.svg index 25ce496f24..220a4a970f 100644 --- a/docs/images/layout/horizontal.excalidraw.svg +++ b/docs/images/layout/horizontal.excalidraw.svg @@ -13,4 +13,4 @@ } - WidgetWidgetWidget \ No newline at end of file + WidgetWidgetWidget diff --git a/docs/images/layout/offset.excalidraw.svg b/docs/images/layout/offset.excalidraw.svg index 7e56bed904..63826c4ac0 100644 --- a/docs/images/layout/offset.excalidraw.svg +++ b/docs/images/layout/offset.excalidraw.svg @@ -13,4 +13,4 @@ } - Offset \ No newline at end of file + Offset diff --git a/docs/images/layout/vertical.excalidraw.svg b/docs/images/layout/vertical.excalidraw.svg index fff71ecf95..7e4b266c45 100644 --- a/docs/images/layout/vertical.excalidraw.svg +++ b/docs/images/layout/vertical.excalidraw.svg @@ -13,4 +13,4 @@ } - WidgetWidgetWidget \ No newline at end of file + WidgetWidgetWidget diff --git a/docs/images/screens/pop_screen.excalidraw.svg b/docs/images/screens/pop_screen.excalidraw.svg index b87957f6a1..6afb8b63b2 100644 --- a/docs/images/screens/pop_screen.excalidraw.svg +++ b/docs/images/screens/pop_screen.excalidraw.svg @@ -13,4 +13,4 @@ } - Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible) \ No newline at end of file + Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible) diff --git a/docs/images/screens/push_screen.excalidraw.svg b/docs/images/screens/push_screen.excalidraw.svg index 7bfde1748c..7d4cef8afa 100644 --- a/docs/images/screens/push_screen.excalidraw.svg +++ b/docs/images/screens/push_screen.excalidraw.svg @@ -13,4 +13,4 @@ } - Screen 1(hidden)Screen 2 (visible)app.push_screen(screen3)Screen 3 (visible)hidden \ No newline at end of file + Screen 1(hidden)Screen 2 (visible)app.push_screen(screen3)Screen 3 (visible)hidden diff --git a/docs/images/screens/switch_screen.excalidraw.svg b/docs/images/screens/switch_screen.excalidraw.svg index beddc0d3b3..7134327ca8 100644 --- a/docs/images/screens/switch_screen.excalidraw.svg +++ b/docs/images/screens/switch_screen.excalidraw.svg @@ -13,4 +13,4 @@ } - Screen 1(hidden)Screen 2 (visible)app.switch_screen(screen3)Screen 3 (visible)Screen 2 removed \ No newline at end of file + Screen 1(hidden)Screen 2 (visible)app.switch_screen(screen3)Screen 3 (visible)Screen 2 removed diff --git a/docs/images/stopwatch.excalidraw.svg b/docs/images/stopwatch.excalidraw.svg index 365a10ff51..8adb11e764 100644 --- a/docs/images/stopwatch.excalidraw.svg +++ b/docs/images/stopwatch.excalidraw.svg @@ -13,4 +13,4 @@ } - StopReset00:00:07.21Start00:00:00.00HeaderFooterStart00:00:00.00StopwatchStopwatch(started)Reset \ No newline at end of file + StopReset00:00:07.21Start00:00:00.00HeaderFooterStart00:00:00.00StopwatchStopwatch(started)Reset diff --git a/docs/images/styles/border_box.excalidraw.svg b/docs/images/styles/border_box.excalidraw.svg index 1d488e24ad..530331d941 100644 --- a/docs/images/styles/border_box.excalidraw.svg +++ b/docs/images/styles/border_box.excalidraw.svg @@ -13,4 +13,4 @@ } - MarginPaddingContent areaBorderHeightWidth \ No newline at end of file + MarginPaddingContent areaBorderHeightWidth diff --git a/docs/images/styles/box.excalidraw.svg b/docs/images/styles/box.excalidraw.svg index 56ffe17585..456971f7fc 100644 --- a/docs/images/styles/box.excalidraw.svg +++ b/docs/images/styles/box.excalidraw.svg @@ -13,4 +13,4 @@ } - MarginPaddingContent areaBorderHeightWidth \ No newline at end of file + MarginPaddingContent areaBorderHeightWidth diff --git a/docs/images/styles/content_box.excalidraw.svg b/docs/images/styles/content_box.excalidraw.svg index ac94e2e6d8..34075b5db4 100644 --- a/docs/images/styles/content_box.excalidraw.svg +++ b/docs/images/styles/content_box.excalidraw.svg @@ -13,4 +13,4 @@ } - MarginPaddingContent areaBorderHeightWidth \ No newline at end of file + MarginPaddingContent areaBorderHeightWidth diff --git a/examples/json_tree.py b/examples/json_tree.py index a8bfd1bdbe..62033b971a 100644 --- a/examples/json_tree.py +++ b/examples/json_tree.py @@ -4,7 +4,7 @@ from rich.text import Text from textual.app import App, ComposeResult -from textual.widgets import Header, Footer, Tree +from textual.widgets import Footer, Header, Tree from textual.widgets.tree import TreeNode diff --git a/src/textual/_tree_sitter.py b/src/textual/_tree_sitter.py index 9d099c109b..c3dbee4f9c 100644 --- a/src/textual/_tree_sitter.py +++ b/src/textual/_tree_sitter.py @@ -1,9 +1,9 @@ from __future__ import annotations + from importlib import import_module from textual import log - try: from tree_sitter import Language diff --git a/src/textual/widgets/text_area.py b/src/textual/widgets/text_area.py index a879e2594c..857618564c 100644 --- a/src/textual/widgets/text_area.py +++ b/src/textual/widgets/text_area.py @@ -12,13 +12,13 @@ from textual.document._syntax_aware_document import SyntaxAwareDocument from textual.document._wrapped_document import WrappedDocument from textual.widgets._text_area import ( + BUILTIN_LANGUAGES, EndColumn, Highlight, HighlightName, LanguageDoesNotExist, StartColumn, ThemeDoesNotExist, - BUILTIN_LANGUAGES, ) __all__ = [ diff --git a/tests/input/test_input_restrict.py b/tests/input/test_input_restrict.py index 48d9d50ee7..d0465ec2e2 100644 --- a/tests/input/test_input_restrict.py +++ b/tests/input/test_input_restrict.py @@ -38,7 +38,6 @@ def test_input_number_type(): assert not re.fullmatch(number, "-inf") - def test_input_integer_type(): """Test input type regex""" integer = _RESTRICT_TYPES["integer"] diff --git a/tests/option_list/test_option_prompt_replacement.py b/tests/option_list/test_option_prompt_replacement.py index ef11b75383..4bcb2f7ca5 100644 --- a/tests/option_list/test_option_prompt_replacement.py +++ b/tests/option_list/test_option_prompt_replacement.py @@ -1,4 +1,5 @@ """Test replacing options prompt from an option list.""" + import pytest from textual.app import App, ComposeResult @@ -20,14 +21,18 @@ async def test_replace_option_prompt_with_invalid_id() -> None: """Attempting to replace the prompt of an option ID that doesn't exist should raise an exception.""" async with OptionListApp().run_test() as pilot: with pytest.raises(OptionDoesNotExist): - pilot.app.query_one(OptionList).replace_option_prompt("does-not-exist", "new-prompt") + pilot.app.query_one(OptionList).replace_option_prompt( + "does-not-exist", "new-prompt" + ) async def test_replace_option_prompt_with_invalid_index() -> None: """Attempting to replace the prompt of an option index that doesn't exist should raise an exception.""" async with OptionListApp().run_test() as pilot: with pytest.raises(OptionDoesNotExist): - pilot.app.query_one(OptionList).replace_option_prompt_at_index(23, "new-prompt") + pilot.app.query_one(OptionList).replace_option_prompt_at_index( + 23, "new-prompt" + ) async def test_replace_option_prompt_with_valid_id() -> None: @@ -41,12 +46,14 @@ async def test_replace_option_prompt_with_valid_id() -> None: async def test_replace_option_prompt_with_valid_index() -> None: """It should be possible to replace the prompt of an option index that does exist.""" async with OptionListApp().run_test() as pilot: - option_list = pilot.app.query_one(OptionList).replace_option_prompt_at_index(1, "new-prompt") + option_list = pilot.app.query_one(OptionList).replace_option_prompt_at_index( + 1, "new-prompt" + ) assert option_list.get_option_at_index(1).prompt == "new-prompt" async def test_replace_single_line_option_prompt_with_multiple() -> None: - """It should be possible to replace single line prompt with multiple lines """ + """It should be possible to replace single line prompt with multiple lines""" new_prompt = "new-prompt\nsecond line" async with OptionListApp().run_test() as pilot: option_list = pilot.app.query_one(OptionList) diff --git a/tests/select/test_blank_and_clear.py b/tests/select/test_blank_and_clear.py index 4d1921451f..3d4f7dca66 100644 --- a/tests/select/test_blank_and_clear.py +++ b/tests/select/test_blank_and_clear.py @@ -64,6 +64,7 @@ def compose(self): with pytest.raises(InvalidSelectValueError): select.clear() + async def test_selection_is_none_with_blank(): class SelectApp(App[None]): def compose(self): diff --git a/tests/selection_list/test_selection_click_checkbox.py b/tests/selection_list/test_selection_click_checkbox.py index e9c349e46b..8ca8b0fe0f 100644 --- a/tests/selection_list/test_selection_click_checkbox.py +++ b/tests/selection_list/test_selection_click_checkbox.py @@ -5,6 +5,7 @@ from textual.geometry import Offset from textual.widgets import SelectionList + class SelectionListApp(App[None]): """Test selection list application.""" @@ -25,7 +26,7 @@ async def test_click_on_prompt() -> None: """It should be possible to toggle a selection by clicking on the prompt.""" async with SelectionListApp().run_test() as pilot: assert isinstance(pilot.app, SelectionListApp) - await pilot.click(SelectionList, Offset(5,1)) + await pilot.click(SelectionList, Offset(5, 1)) await pilot.pause() assert pilot.app.clicks == [0] @@ -34,9 +35,10 @@ async def test_click_on_checkbox() -> None: """It should be possible to toggle a selection by clicking on the checkbox.""" async with SelectionListApp().run_test() as pilot: assert isinstance(pilot.app, SelectionListApp) - await pilot.click(SelectionList, Offset(3,1)) + await pilot.click(SelectionList, Offset(3, 1)) await pilot.pause() assert pilot.app.clicks == [0] + if __name__ == "__main__": SelectionListApp().run() diff --git a/tests/test_expand_tabs.py b/tests/test_expand_tabs.py index b120978da5..811803b7a5 100644 --- a/tests/test_expand_tabs.py +++ b/tests/test_expand_tabs.py @@ -29,4 +29,9 @@ def test_get_tab_widths(): assert get_tab_widths("\tbar") == [("", 4), ("bar", 0)] assert get_tab_widths("\tbar\t") == [("", 4), ("bar", 1)] assert get_tab_widths("\tfoo\t\t") == [("", 4), ("foo", 1), ("", 4)] - assert get_tab_widths("\t木foo\t木\t\t") == [("", 4), ("木foo", 3), ("木", 2), ("", 4)] + assert get_tab_widths("\t木foo\t木\t\t") == [ + ("", 4), + ("木foo", 3), + ("木", 2), + ("", 4), + ] diff --git a/tests/test_unmount.py b/tests/test_unmount.py index 3da75d1247..324a3812a3 100644 --- a/tests/test_unmount.py +++ b/tests/test_unmount.py @@ -50,5 +50,4 @@ async def on_mount(self) -> None: "MyScreen#main", ] - assert unmount_ids == expected diff --git a/tests/test_validation.py b/tests/test_validation.py index 38be8b38e9..6a6bee2f6a 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -160,7 +160,12 @@ def test_Regex_validate(regex, value, expected_result): ("123", 100, 200, True), # valid integer within range ("99", 100, 200, False), # valid integer but not in range ("201", 100, 200, False), # valid integer but not in range - ("1.23e4", None, None, False), # valid scientific notation, even resolving to an integer, is not valid + ( + "1.23e4", + None, + None, + False, + ), # valid scientific notation, even resolving to an integer, is not valid ("123.", None, None, False), # periods not valid in integers ("123_456", None, None, True), # underscores are valid python ("_123_456", None, None, False), # leading underscores are not valid python diff --git a/tests/test_win_sleep.py b/tests/test_win_sleep.py index 5ed9abbd83..fdbc9ae178 100644 --- a/tests/test_win_sleep.py +++ b/tests/test_win_sleep.py @@ -1,6 +1,6 @@ import asyncio -import time import sys +import time import pytest diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index e732680f0f..d4f8241ed3 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -115,9 +115,7 @@ async def test_insert_character_near_cursor_maintain_selection_offset( ], ) async def test_insert_newline_around_cursor_maintain_selection_offset( - cursor_location, - insert_location, - cursor_destination + cursor_location, insert_location, cursor_destination ): app = TextAreaApp() async with app.run_test(): diff --git a/tests/text_area/test_selection_bindings.py b/tests/text_area/test_selection_bindings.py index 4fd6947386..5985635cc6 100644 --- a/tests/text_area/test_selection_bindings.py +++ b/tests/text_area/test_selection_bindings.py @@ -254,9 +254,7 @@ async def test_cursor_page_down(app: TextAreaApp): text_area.selection = Selection.cursor((0, 1)) await pilot.press("pagedown") margin = 2 - assert text_area.selection == Selection.cursor( - (app.size.height - margin, 1) - ) + assert text_area.selection == Selection.cursor((app.size.height - margin, 1)) async def test_cursor_page_up(app: TextAreaApp): diff --git a/tests/tree/test_node_refresh.py b/tests/tree/test_node_refresh.py index 53e98b7387..8efaaec1ed 100644 --- a/tests/tree/test_node_refresh.py +++ b/tests/tree/test_node_refresh.py @@ -5,6 +5,7 @@ from textual.widgets import Tree from textual.widgets.tree import TreeNode + class HistoryTree(Tree): def __init__(self) -> None: @@ -33,7 +34,7 @@ async def test_initial_state() -> None: """Initially all the visible nodes should have had a render call.""" app = RefreshApp() async with app.run_test(): - assert app.query_one(HistoryTree).render_hits == {(0,0), (1,0), (2,0)} + assert app.query_one(HistoryTree).render_hits == {(0, 0), (1, 0), (2, 0)} async def test_root_refresh() -> None: @@ -45,6 +46,7 @@ async def test_root_refresh() -> None: await pilot.pause() assert (0, 1) in pilot.app.query_one(HistoryTree).render_hits + async def test_child_refresh() -> None: """A refresh of the child node should cause a subsequent render call.""" async with RefreshApp().run_test() as pilot: @@ -54,6 +56,7 @@ async def test_child_refresh() -> None: await pilot.pause() assert (1, 1) in pilot.app.query_one(HistoryTree).render_hits + async def test_grandchild_refresh() -> None: """A refresh of the grandchild node should cause a subsequent render call.""" async with RefreshApp().run_test() as pilot: diff --git a/tools/widget_documentation.py b/tools/widget_documentation.py index 04f3de86bf..7149e6d465 100644 --- a/tools/widget_documentation.py +++ b/tools/widget_documentation.py @@ -4,6 +4,7 @@ This goes through the widgets listed in textual.widgets and prints the scaffolding for the tables that are used to document the classvars BINDINGS and COMPONENT_CLASSES. """ + from __future__ import annotations from typing import TYPE_CHECKING From 87cc68db84532d7f3876a3e4751c635a11d11569 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 7 Apr 2025 15:00:30 +0100 Subject: [PATCH 02/14] fix vcs permalink --- docs/blog/posts/release0-24-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/posts/release0-24-0.md b/docs/blog/posts/release0-24-0.md index 95feb66fc9..8addc95216 100644 --- a/docs/blog/posts/release0-24-0.md +++ b/docs/blog/posts/release0-24-0.md @@ -50,7 +50,7 @@ This was also solved with a new rule called "constrain". Applying `constrain` to a widget will keep the widget within the bounds of the screen. In the case of `Select`, if you expand the options while at the bottom of the screen, then the overlay will be moved up so that you can see all the options. -These new rules are currently undocumented as they are still subject to change, but you can see them in the [Select](https://github.com/Textualize/textual/blob/main/src/textual/widgets/_select.py#L179) source if you are interested. +These new rules are currently undocumented as they are still subject to change, but you can see them in the [Select](https://github.com/Textualize/textual/blob/v0.24.0/src/textual/widgets/_select.py#L179-L220) source if you are interested. In a future release these will be finalized and you can confidently use them in your own projects. From b3e019256774b72c675dc19bd829da90ba8cb1f7 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 7 Apr 2025 15:00:58 +0100 Subject: [PATCH 03/14] pre-commit autoupdate --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 009c06616e..47d992ed03 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v5.0.0 hooks: - id: check-ast # simply checks whether the files parse as valid python - id: check-builtin-literals # requires literal syntax when initializing empty or zero python builtin types @@ -17,18 +17,18 @@ repos: - id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline - id: mixed-line-ending # replaces or checks mixed line ending - repo: https://github.com/pycqa/isort - rev: '5.13.2' + rev: '6.0.1' hooks: - id: isort name: isort (python) language_version: '3.11' args: ['--profile', 'black', '--filter-files'] - repo: https://github.com/psf/black - rev: '24.1.1' + rev: '25.1.0' hooks: - id: black - repo: https://github.com/hadialqattan/pycln # removes unused imports - rev: v2.3.0 + rev: v2.5.0 hooks: - id: pycln language_version: '3.11' From 5ecbf61e90c330acbf117efd298b52927af6a9bc Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 7 Apr 2025 16:27:29 +0100 Subject: [PATCH 04/14] restore SVGs from main --- docs/blog/images/faster-updates.excalidraw.svg | 2 +- docs/images/actions/format.excalidraw.svg | 2 +- docs/images/animation/animation.excalidraw.svg | 2 +- docs/images/child_combinator.excalidraw.svg | 2 +- docs/images/css_stopwatch.excalidraw.svg | 2 +- docs/images/descendant_combinator.excalidraw.svg | 2 +- docs/images/dom1.excalidraw.svg | 2 +- docs/images/dom2.excalidraw.svg | 2 +- docs/images/dom3.excalidraw.svg | 2 +- docs/images/events/bubble1.excalidraw.svg | 2 +- docs/images/events/bubble2.excalidraw.svg | 2 +- docs/images/events/bubble3.excalidraw.svg | 2 +- docs/images/events/naming.excalidraw.svg | 2 +- docs/images/events/queue.excalidraw.svg | 2 +- docs/images/events/queue2.excalidraw.svg | 2 +- docs/images/input/coords.excalidraw.svg | 2 +- docs/images/layout/align.excalidraw.svg | 2 +- docs/images/layout/center.excalidraw.svg | 2 +- docs/images/layout/dock.excalidraw.svg | 2 +- docs/images/layout/grid.excalidraw.svg | 2 +- docs/images/layout/horizontal.excalidraw.svg | 2 +- docs/images/layout/offset.excalidraw.svg | 2 +- docs/images/layout/vertical.excalidraw.svg | 2 +- docs/images/screens/pop_screen.excalidraw.svg | 2 +- docs/images/screens/push_screen.excalidraw.svg | 2 +- docs/images/screens/switch_screen.excalidraw.svg | 2 +- docs/images/stopwatch.excalidraw.svg | 2 +- docs/images/styles/border_box.excalidraw.svg | 2 +- docs/images/styles/box.excalidraw.svg | 2 +- docs/images/styles/content_box.excalidraw.svg | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/blog/images/faster-updates.excalidraw.svg b/docs/blog/images/faster-updates.excalidraw.svg index 47586a065c..18340ee26a 100644 --- a/docs/blog/images/faster-updates.excalidraw.svg +++ b/docs/blog/images/faster-updates.excalidraw.svg @@ -13,4 +13,4 @@ } - UpdateWriteUpdateWriteUpdateWriteUpdateWriteBeforeAfterTime + UpdateWriteUpdateWriteUpdateWriteUpdateWriteBeforeAfterTime \ No newline at end of file diff --git a/docs/images/actions/format.excalidraw.svg b/docs/images/actions/format.excalidraw.svg index 7a7bdd6925..b74fbad3fd 100644 --- a/docs/images/actions/format.excalidraw.svg +++ b/docs/images/actions/format.excalidraw.svg @@ -13,4 +13,4 @@ } - Optional namespaceAction nameOptional parametersapp.set_background('red') + Optional namespaceAction nameOptional parametersapp.set_background('red') \ No newline at end of file diff --git a/docs/images/animation/animation.excalidraw.svg b/docs/images/animation/animation.excalidraw.svg index 022b7e1ce9..da9a23f259 100644 --- a/docs/images/animation/animation.excalidraw.svg +++ b/docs/images/animation/animation.excalidraw.svg @@ -13,4 +13,4 @@ } - timevalue + timevalue \ No newline at end of file diff --git a/docs/images/child_combinator.excalidraw.svg b/docs/images/child_combinator.excalidraw.svg index 684dd91739..47e3b0d8e5 100644 --- a/docs/images/child_combinator.excalidraw.svg +++ b/docs/images/child_combinator.excalidraw.svg @@ -13,4 +13,4 @@ } - Container( id="dialog")Horizontal( classes="buttons")Button("Yes")Button("No")Screen()Container( id="sidebar")Button( "Install")Underline this button + Container( id="dialog")Horizontal( classes="buttons")Button("Yes")Button("No")Screen()Container( id="sidebar")Button( "Install")Underline this button \ No newline at end of file diff --git a/docs/images/css_stopwatch.excalidraw.svg b/docs/images/css_stopwatch.excalidraw.svg index ed0e49edbe..931702ccff 100644 --- a/docs/images/css_stopwatch.excalidraw.svg +++ b/docs/images/css_stopwatch.excalidraw.svg @@ -13,4 +13,4 @@ } - Stop00:00:00.00ResetStart00:00:00.00StopwatchStarted Stopwatch + Stop00:00:00.00ResetStart00:00:00.00StopwatchStarted Stopwatch \ No newline at end of file diff --git a/docs/images/descendant_combinator.excalidraw.svg b/docs/images/descendant_combinator.excalidraw.svg index e4eba50a7f..1f5a7ce8d1 100644 --- a/docs/images/descendant_combinator.excalidraw.svg +++ b/docs/images/descendant_combinator.excalidraw.svg @@ -13,4 +13,4 @@ } - Container( id="dialog")Horizontal( classes="buttons")Button("Yes")Button("No")Screen()Container( id="sidebar")Button( "Install")match these*don't* match this + Container( id="dialog")Horizontal( classes="buttons")Button("Yes")Button("No")Screen()Container( id="sidebar")Button( "Install")match these*don't* match this \ No newline at end of file diff --git a/docs/images/dom1.excalidraw.svg b/docs/images/dom1.excalidraw.svg index 063fb10eea..54634dc1de 100644 --- a/docs/images/dom1.excalidraw.svg +++ b/docs/images/dom1.excalidraw.svg @@ -13,4 +13,4 @@ } - ExampleApp()Screen() + ExampleApp()Screen() \ No newline at end of file diff --git a/docs/images/dom2.excalidraw.svg b/docs/images/dom2.excalidraw.svg index 62ffe4dbde..bd636267f4 100644 --- a/docs/images/dom2.excalidraw.svg +++ b/docs/images/dom2.excalidraw.svg @@ -13,4 +13,4 @@ } - ExampleApp()Screen()Header()Footer() + ExampleApp()Screen()Header()Footer() \ No newline at end of file diff --git a/docs/images/dom3.excalidraw.svg b/docs/images/dom3.excalidraw.svg index 6ff78afdb5..86fb5e78d8 100644 --- a/docs/images/dom3.excalidraw.svg +++ b/docs/images/dom3.excalidraw.svg @@ -13,4 +13,4 @@ } - App()Screen()Header()Footer()Container( id="dialog")Horizontal( classes="buttons")Button( "Yes", variant="success")Button( "No", variant="error")Static( QUESTION, classes="questions") + App()Screen()Header()Footer()Container( id="dialog")Horizontal( classes="buttons")Button( "Yes", variant="success")Button( "No", variant="error")Static( QUESTION, classes="questions") \ No newline at end of file diff --git a/docs/images/events/bubble1.excalidraw.svg b/docs/images/events/bubble1.excalidraw.svg index 3f201adfed..f4b79d1011 100644 --- a/docs/images/events/bubble1.excalidraw.svg +++ b/docs/images/events/bubble1.excalidraw.svg @@ -13,4 +13,4 @@ } - App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T") + App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T") \ No newline at end of file diff --git a/docs/images/events/bubble2.excalidraw.svg b/docs/images/events/bubble2.excalidraw.svg index f98666ad45..e3e4d20791 100644 --- a/docs/images/events/bubble2.excalidraw.svg +++ b/docs/images/events/bubble2.excalidraw.svg @@ -13,4 +13,4 @@ } - App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T")events.Key(key="T")bubble + App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T")events.Key(key="T")bubble \ No newline at end of file diff --git a/docs/images/events/bubble3.excalidraw.svg b/docs/images/events/bubble3.excalidraw.svg index 432a4f031d..afc97dc2f0 100644 --- a/docs/images/events/bubble3.excalidraw.svg +++ b/docs/images/events/bubble3.excalidraw.svg @@ -13,4 +13,4 @@ } - App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T")events.Key(key="T")events.Key(key="T")bubble + App()Container( id="dialog")Button( "Yes", variant="success")Button( "No", variant="error")events.Key(key="T")events.Key(key="T")events.Key(key="T")bubble \ No newline at end of file diff --git a/docs/images/events/naming.excalidraw.svg b/docs/images/events/naming.excalidraw.svg index 0a69c6bcce..5a9a2fb4ca 100644 --- a/docs/images/events/naming.excalidraw.svg +++ b/docs/images/events/naming.excalidraw.svg @@ -13,4 +13,4 @@ } - Makes the methoda message handlerMessage namespace(outer class)Name ofmessage classon_color_button_selected + Makes the methoda message handlerMessage namespace(outer class)Name ofmessage classon_color_button_selected \ No newline at end of file diff --git a/docs/images/events/queue.excalidraw.svg b/docs/images/events/queue.excalidraw.svg index 8747674c28..45230f41ba 100644 --- a/docs/images/events/queue.excalidraw.svg +++ b/docs/images/events/queue.excalidraw.svg @@ -13,4 +13,4 @@ } - events.Key(key="T")events.Key(key="e")events.Key(key="x")Message queueon_key(event)Event handlerevents.Key(key="t") + events.Key(key="T")events.Key(key="e")events.Key(key="x")Message queueon_key(event)Event handlerevents.Key(key="t") \ No newline at end of file diff --git a/docs/images/events/queue2.excalidraw.svg b/docs/images/events/queue2.excalidraw.svg index b5fbb6dfde..d4ed6be006 100644 --- a/docs/images/events/queue2.excalidraw.svg +++ b/docs/images/events/queue2.excalidraw.svg @@ -13,4 +13,4 @@ } - events.Key(key="e")events.Key(key="x")events.Key(key="t")Tevents.Key(key="x")events.Key(key="t")Teevents.Key(key="t")TexText + events.Key(key="e")events.Key(key="x")events.Key(key="t")Tevents.Key(key="x")events.Key(key="t")Teevents.Key(key="t")TexText \ No newline at end of file diff --git a/docs/images/input/coords.excalidraw.svg b/docs/images/input/coords.excalidraw.svg index 7a4114e20d..fb925e22c3 100644 --- a/docs/images/input/coords.excalidraw.svg +++ b/docs/images/input/coords.excalidraw.svg @@ -13,4 +13,4 @@ } - XyXy(0, 0)(0, 0)Widget + XyXy(0, 0)(0, 0)Widget \ No newline at end of file diff --git a/docs/images/layout/align.excalidraw.svg b/docs/images/layout/align.excalidraw.svg index 2dd2ac4fc7..fd2f8e94fb 100644 --- a/docs/images/layout/align.excalidraw.svg +++ b/docs/images/layout/align.excalidraw.svg @@ -13,4 +13,4 @@ } - align-horizontal: leftalign-horizontal: centeralign-horizontal: right + align-horizontal: leftalign-horizontal: centeralign-horizontal: right \ No newline at end of file diff --git a/docs/images/layout/center.excalidraw.svg b/docs/images/layout/center.excalidraw.svg index 0420f1aeaa..b9e15e7a9b 100644 --- a/docs/images/layout/center.excalidraw.svg +++ b/docs/images/layout/center.excalidraw.svg @@ -13,4 +13,4 @@ } - Widget + Widget \ No newline at end of file diff --git a/docs/images/layout/dock.excalidraw.svg b/docs/images/layout/dock.excalidraw.svg index ffded907a1..fde040991a 100644 --- a/docs/images/layout/dock.excalidraw.svg +++ b/docs/images/layout/dock.excalidraw.svg @@ -13,4 +13,4 @@ } - Docked widgetLayout widgets + Docked widgetLayout widgets \ No newline at end of file diff --git a/docs/images/layout/grid.excalidraw.svg b/docs/images/layout/grid.excalidraw.svg index c295751e87..aed2f9d24f 100644 --- a/docs/images/layout/grid.excalidraw.svg +++ b/docs/images/layout/grid.excalidraw.svg @@ -13,4 +13,4 @@ } - + \ No newline at end of file diff --git a/docs/images/layout/horizontal.excalidraw.svg b/docs/images/layout/horizontal.excalidraw.svg index 220a4a970f..25ce496f24 100644 --- a/docs/images/layout/horizontal.excalidraw.svg +++ b/docs/images/layout/horizontal.excalidraw.svg @@ -13,4 +13,4 @@ } - WidgetWidgetWidget + WidgetWidgetWidget \ No newline at end of file diff --git a/docs/images/layout/offset.excalidraw.svg b/docs/images/layout/offset.excalidraw.svg index 63826c4ac0..7e56bed904 100644 --- a/docs/images/layout/offset.excalidraw.svg +++ b/docs/images/layout/offset.excalidraw.svg @@ -13,4 +13,4 @@ } - Offset + Offset \ No newline at end of file diff --git a/docs/images/layout/vertical.excalidraw.svg b/docs/images/layout/vertical.excalidraw.svg index 7e4b266c45..fff71ecf95 100644 --- a/docs/images/layout/vertical.excalidraw.svg +++ b/docs/images/layout/vertical.excalidraw.svg @@ -13,4 +13,4 @@ } - WidgetWidgetWidget + WidgetWidgetWidget \ No newline at end of file diff --git a/docs/images/screens/pop_screen.excalidraw.svg b/docs/images/screens/pop_screen.excalidraw.svg index 6afb8b63b2..b87957f6a1 100644 --- a/docs/images/screens/pop_screen.excalidraw.svg +++ b/docs/images/screens/pop_screen.excalidraw.svg @@ -13,4 +13,4 @@ } - Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible) + Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible) \ No newline at end of file diff --git a/docs/images/screens/push_screen.excalidraw.svg b/docs/images/screens/push_screen.excalidraw.svg index 7d4cef8afa..7bfde1748c 100644 --- a/docs/images/screens/push_screen.excalidraw.svg +++ b/docs/images/screens/push_screen.excalidraw.svg @@ -13,4 +13,4 @@ } - Screen 1(hidden)Screen 2 (visible)app.push_screen(screen3)Screen 3 (visible)hidden + Screen 1(hidden)Screen 2 (visible)app.push_screen(screen3)Screen 3 (visible)hidden \ No newline at end of file diff --git a/docs/images/screens/switch_screen.excalidraw.svg b/docs/images/screens/switch_screen.excalidraw.svg index 7134327ca8..beddc0d3b3 100644 --- a/docs/images/screens/switch_screen.excalidraw.svg +++ b/docs/images/screens/switch_screen.excalidraw.svg @@ -13,4 +13,4 @@ } - Screen 1(hidden)Screen 2 (visible)app.switch_screen(screen3)Screen 3 (visible)Screen 2 removed + Screen 1(hidden)Screen 2 (visible)app.switch_screen(screen3)Screen 3 (visible)Screen 2 removed \ No newline at end of file diff --git a/docs/images/stopwatch.excalidraw.svg b/docs/images/stopwatch.excalidraw.svg index 8adb11e764..365a10ff51 100644 --- a/docs/images/stopwatch.excalidraw.svg +++ b/docs/images/stopwatch.excalidraw.svg @@ -13,4 +13,4 @@ } - StopReset00:00:07.21Start00:00:00.00HeaderFooterStart00:00:00.00StopwatchStopwatch(started)Reset + StopReset00:00:07.21Start00:00:00.00HeaderFooterStart00:00:00.00StopwatchStopwatch(started)Reset \ No newline at end of file diff --git a/docs/images/styles/border_box.excalidraw.svg b/docs/images/styles/border_box.excalidraw.svg index 530331d941..1d488e24ad 100644 --- a/docs/images/styles/border_box.excalidraw.svg +++ b/docs/images/styles/border_box.excalidraw.svg @@ -13,4 +13,4 @@ } - MarginPaddingContent areaBorderHeightWidth + MarginPaddingContent areaBorderHeightWidth \ No newline at end of file diff --git a/docs/images/styles/box.excalidraw.svg b/docs/images/styles/box.excalidraw.svg index 456971f7fc..56ffe17585 100644 --- a/docs/images/styles/box.excalidraw.svg +++ b/docs/images/styles/box.excalidraw.svg @@ -13,4 +13,4 @@ } - MarginPaddingContent areaBorderHeightWidth + MarginPaddingContent areaBorderHeightWidth \ No newline at end of file diff --git a/docs/images/styles/content_box.excalidraw.svg b/docs/images/styles/content_box.excalidraw.svg index 34075b5db4..ac94e2e6d8 100644 --- a/docs/images/styles/content_box.excalidraw.svg +++ b/docs/images/styles/content_box.excalidraw.svg @@ -13,4 +13,4 @@ } - MarginPaddingContent areaBorderHeightWidth + MarginPaddingContent areaBorderHeightWidth \ No newline at end of file From 945f418647321cf5c46cb061284126dad19bee65 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 7 Apr 2025 16:34:51 +0100 Subject: [PATCH 05/14] exclude svg from end of file fixer --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47d992ed03..ce352e11b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,6 +15,7 @@ repos: - id: check-shebang-scripts-are-executable # ensures that (non-binary) files with a shebang are executable - id: check-vcs-permalinks # ensures that links to vcs websites are permalinks - id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline + exclude: '^.*\.svg$' - id: mixed-line-ending # replaces or checks mixed line ending - repo: https://github.com/pycqa/isort rev: '6.0.1' From f5c221de26b56a191f0848b82f5fde7d8ad6e8e2 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 8 Apr 2025 08:03:17 +0100 Subject: [PATCH 06/14] fix widgets.md --- docs/guide/widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 4c83316ffe..4072fe4a47 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -511,7 +511,7 @@ Let's add scrolling to our checkerboard example. A standard 8 x 8 board isn't su === "checker03.py" - ```python title="checker03.py" hl_lines="4 26-30 35-36 52-53" + ```python title="checker03.py" hl_lines="6 26-30 35-36 52-53" --8<-- "docs/examples/guide/widgets/checker03.py" ``` From 50800102c3974113bf0c25820f495784ff4d422a Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 8 Apr 2025 08:05:58 +0100 Subject: [PATCH 07/14] fix styles.md --- docs/guide/styles.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/guide/styles.md b/docs/guide/styles.md index c251f9faf4..dfa5f675ad 100644 --- a/docs/guide/styles.md +++ b/docs/guide/styles.md @@ -125,7 +125,7 @@ Together these styles compose the widget's *box model*. The following diagram sh Setting the width restricts the number of columns used by a widget, and setting the height restricts the number of rows. Let's look at an example which sets both dimensions. -```python title="dimensions01.py" hl_lines="21-22" +```python title="dimensions01.py" hl_lines="20-21" --8<-- "docs/examples/guide/styles/dimensions01.py" ``` @@ -142,7 +142,7 @@ In practice, we generally want the size of a widget to adapt to its content, whi Let's set the height to auto and see what happens. -```python title="dimensions02.py" hl_lines="22" +```python title="dimensions02.py" hl_lines="21" --8<-- "docs/examples/guide/styles/dimensions02.py" ``` @@ -163,7 +163,7 @@ Textual offers a few different *units* which allow you to specify dimensions rel The following example demonstrates applying percentage units: -```python title="dimensions03.py" hl_lines="21-22" +```python title="dimensions03.py" hl_lines="20-21" --8<-- "docs/examples/guide/styles/dimensions03.py" ``` @@ -193,7 +193,7 @@ When specifying `fr` units for a given dimension, Textual will divide the availa Let's look at an example. We will create two widgets, one with a height of `"2fr"` and one with a height of `"1fr"`. -```python title="dimensions04.py" hl_lines="24-25" +```python title="dimensions04.py" hl_lines="23-24" --8<-- "docs/examples/guide/styles/dimensions04.py" ``` @@ -219,7 +219,7 @@ The following styles set minimum and maximum sizes and can accept any of the val Padding adds space around your content which can aid readability. Setting [padding](../styles/padding.md) to an integer will add that number additional rows and columns around the content area. The following example sets padding to 2: -```python title="padding01.py" hl_lines="22" +```python title="padding01.py" hl_lines="21" --8<-- "docs/examples/guide/styles/padding01.py" ``` @@ -230,7 +230,7 @@ Notice the additional space around the text: You can also set padding to a tuple of *two* integers which will apply padding to the top/bottom and left/right edges. The following example sets padding to `(2, 4)` which adds two rows to the top and bottom of the widget, and 4 columns to the left and right of the widget. -```python title="padding02.py" hl_lines="22" +```python title="padding02.py" hl_lines="21" --8<-- "docs/examples/guide/styles/padding02.py" ``` @@ -325,7 +325,7 @@ The following example creates two widgets with a width of 30, a height of 6, and The first widget has the default `box_sizing` (`"border-box"`). The second widget sets `box_sizing` to `"content-box"`. -```python title="box_sizing01.py" hl_lines="32" +```python title="box_sizing01.py" hl_lines="31" --8<-- "docs/examples/guide/styles/box_sizing01.py" ``` @@ -340,7 +340,7 @@ Margin is similar to padding in that it adds space, but unlike padding, [margin] The following example creates two widgets, each with a margin of 2. -```python title="margin01.py" hl_lines="26-27" +```python title="margin01.py" hl_lines="25-26" --8<-- "docs/examples/guide/styles/margin01.py" ``` From bfbcfb7ffd44ac098998bc5b54c648ba30f0f587 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 8 Apr 2025 18:26:17 +0100 Subject: [PATCH 08/14] Update styles.md --- docs/guide/styles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/styles.md b/docs/guide/styles.md index dfa5f675ad..abb8d81f98 100644 --- a/docs/guide/styles.md +++ b/docs/guide/styles.md @@ -289,7 +289,7 @@ Note the addition of the titles and their alignments: [Outline](../styles/outline.md) is similar to border and is set in the same way. The difference is that outline will not change the size of the widget, and may overlap the content area. The following example sets an outline on a widget: -```python title="outline01.py" hl_lines="22" +```python title="outline01.py" hl_lines="21" --8<-- "docs/examples/guide/styles/outline01.py" ``` From f8ef8bcf9c3eb446d78309d74cd2a605865a19da Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 24 Apr 2025 10:17:04 +0100 Subject: [PATCH 09/14] remove pytest-asyncio --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c01b22558b..7d564f62c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,7 +107,6 @@ mypy = "^1.0.0" pre-commit = "^2.13.0" pytest = "^8.3.1" pytest-xdist = "^3.6.1" -pytest-asyncio = "*" pytest-cov = "^5.0.0" textual-dev = "^1.7.0" types-setuptools = "^67.2.0.1" @@ -115,13 +114,11 @@ isort = "^5.13.2" pytest-textual-snapshot = "^1.0.0" [tool.pytest.ini_options] -asyncio_mode = "auto" testpaths = ["tests"] addopts = "--strict-markers" markers = [ "syntax: marks tests that require syntax highlighting (deselect with '-m \"not syntax\"')", ] -asyncio_default_fixture_loop_scope = "function" [build-system] requires = ["poetry-core>=1.2.0"] From b0ca2af7505c54d31a024a0312dbf4df3fa68e9e Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 24 Apr 2025 10:20:16 +0100 Subject: [PATCH 10/14] configure anyio backend --- tests/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..736583850a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +import pytest + +@pytest.fixture +def anyio_backend() -> object: + return ("asyncio", {"debug": True}) From 595721805fe7a3e844fb2c8904339c12edc1bb1a Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 24 Apr 2025 10:22:15 +0100 Subject: [PATCH 11/14] autoflake --- src/textual/_win_sleep.py | 1 - src/textual/app.py | 2 -- src/textual/suggester.py | 1 - tests/snapshot_tests/snapshot_apps/data_table_add_column.py | 2 +- tests/snapshot_tests/snapshot_apps/directory_tree_reload.py | 2 +- tests/snapshot_tests/snapshot_apps/log_write.py | 1 - tests/snapshot_tests/snapshot_apps/mount_style_fix.py | 1 - tests/snapshot_tests/snapshot_apps/scroll_page.py | 1 - tests/snapshot_tests/snapshot_apps/text_selection.py | 1 - tests/test_widget.py | 1 - 10 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/textual/_win_sleep.py b/src/textual/_win_sleep.py index c8f938c1fc..2c086a023a 100644 --- a/src/textual/_win_sleep.py +++ b/src/textual/_win_sleep.py @@ -39,7 +39,6 @@ def sleep(secs: float) -> Coroutine[None, None, None]: async def no_sleep_coro(): """Creates a coroutine that does nothing for when no sleep is needed.""" - pass def sleep(secs: float) -> Coroutine[None, None, None]: """A replacement sleep for Windows. diff --git a/src/textual/app.py b/src/textual/app.py index 6861012863..69d9ea4977 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -131,7 +131,6 @@ from textual.worker_manager import WorkerManager if TYPE_CHECKING: - from textual_dev.client import DevtoolsClient from typing_extensions import Coroutine, Literal, Self, TypeAlias from textual._types import MessageTarget @@ -3752,7 +3751,6 @@ def handle_bindings_clash( clashed_bindings: The bindings that are clashing. node: The node that has the clashing bindings. """ - pass async def on_event(self, event: events.Event) -> None: # Handle input events that haven't been forwarded diff --git a/src/textual/suggester.py b/src/textual/suggester.py index 89557e3f7d..c7a5d26635 100644 --- a/src/textual/suggester.py +++ b/src/textual/suggester.py @@ -90,7 +90,6 @@ async def get_suggestion(self, value: str) -> str | None: Returns: A valid suggestion or `None`. """ - pass class SuggestFromList(Suggester): diff --git a/tests/snapshot_tests/snapshot_apps/data_table_add_column.py b/tests/snapshot_tests/snapshot_apps/data_table_add_column.py index d80af7eebf..a792a195a4 100644 --- a/tests/snapshot_tests/snapshot_apps/data_table_add_column.py +++ b/tests/snapshot_tests/snapshot_apps/data_table_add_column.py @@ -1,6 +1,6 @@ from textual.app import App, ComposeResult from textual.binding import Binding -from textual.widgets import DataTable, Footer +from textual.widgets import DataTable MOVIES = [ "Severance", diff --git a/tests/snapshot_tests/snapshot_apps/directory_tree_reload.py b/tests/snapshot_tests/snapshot_apps/directory_tree_reload.py index f8c0dee21f..1603637952 100644 --- a/tests/snapshot_tests/snapshot_apps/directory_tree_reload.py +++ b/tests/snapshot_tests/snapshot_apps/directory_tree_reload.py @@ -1,6 +1,6 @@ from pathlib import Path -from textual.app import App, ComposeResult +from textual.app import App from textual.widgets import DirectoryTree diff --git a/tests/snapshot_tests/snapshot_apps/log_write.py b/tests/snapshot_tests/snapshot_apps/log_write.py index dbca071221..6090bd1f76 100644 --- a/tests/snapshot_tests/snapshot_apps/log_write.py +++ b/tests/snapshot_tests/snapshot_apps/log_write.py @@ -1,6 +1,5 @@ from textual.app import App, ComposeResult from textual.widgets import Log -from textual.containers import Horizontal class LogApp(App): diff --git a/tests/snapshot_tests/snapshot_apps/mount_style_fix.py b/tests/snapshot_tests/snapshot_apps/mount_style_fix.py index 038ce75ba6..8aafe10f6c 100644 --- a/tests/snapshot_tests/snapshot_apps/mount_style_fix.py +++ b/tests/snapshot_tests/snapshot_apps/mount_style_fix.py @@ -1,4 +1,3 @@ -from textual import __version__ from textual.app import App, ComposeResult from textual.widgets import Static diff --git a/tests/snapshot_tests/snapshot_apps/scroll_page.py b/tests/snapshot_tests/snapshot_apps/scroll_page.py index 33243fef5c..29280a0249 100644 --- a/tests/snapshot_tests/snapshot_apps/scroll_page.py +++ b/tests/snapshot_tests/snapshot_apps/scroll_page.py @@ -1,4 +1,3 @@ -import os from textual.app import App from textual.widgets import RichLog diff --git a/tests/snapshot_tests/snapshot_apps/text_selection.py b/tests/snapshot_tests/snapshot_apps/text_selection.py index d76ce89e9b..5b012a74f0 100644 --- a/tests/snapshot_tests/snapshot_apps/text_selection.py +++ b/tests/snapshot_tests/snapshot_apps/text_selection.py @@ -3,7 +3,6 @@ from textual.widgets import Label from textual.containers import VerticalGroup -from textual.content import Content TEXT = """I must not fear. Fear is the mind-killer. diff --git a/tests/test_widget.py b/tests/test_widget.py index cb51bbb65e..4f2918d1ce 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -628,7 +628,6 @@ def test_lazy_loading() -> None: from textual.widgets import Foo # nopycln: import from textual import widgets - from textual.widgets import Label assert not hasattr(widgets, "foo") assert not hasattr(widgets, "bar") From 76f5554eaba6643a7cedc46f5c72cf95167dcc7f Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 24 Apr 2025 10:25:15 +0100 Subject: [PATCH 12/14] add anyio mark --- docs/examples/guide/testing/test_rgb.py | 2 + tests/animations/test_disabling_animations.py | 6 ++ .../test_loading_indicator_animation.py | 3 + .../animations/test_progress_bar_animation.py | 3 + tests/animations/test_scrolling_animation.py | 3 + tests/animations/test_switch_animation.py | 3 + .../test_tabs_underline_animation.py | 3 + tests/command_palette/test_click_away.py | 1 + .../test_command_source_environment.py | 1 + tests/command_palette/test_declare_sources.py | 6 ++ tests/command_palette/test_discover.py | 1 + tests/command_palette/test_escaping.py | 1 + tests/command_palette/test_events.py | 4 ++ tests/command_palette/test_interaction.py | 3 + tests/command_palette/test_no_results.py | 1 + tests/command_palette/test_run_on_select.py | 2 + .../test_worker_interference.py | 2 + tests/css/test_css_reloading.py | 2 + tests/css/test_inheritance.py | 1 + tests/css/test_initial.py | 2 + tests/css/test_nested_css.py | 4 ++ tests/css/test_programmatic_style_changes.py | 2 + tests/css/test_screen_css.py | 10 +++ tests/directory_tree/test_change_path.py | 1 + tests/directory_tree/test_early_show_root.py | 1 + tests/footer/test_footer.py | 1 + tests/input/test_cut_copy_paste.py | 3 + tests/input/test_input_clear.py | 1 + .../test_input_key_modification_actions.py | 15 ++++ .../input/test_input_key_movement_actions.py | 14 ++++ tests/input/test_input_messages.py | 7 ++ tests/input/test_input_mouse.py | 2 + tests/input/test_input_properties.py | 7 ++ tests/input/test_input_replace.py | 2 + tests/input/test_input_restrict.py | 4 ++ tests/input/test_input_terminal_cursor.py | 2 + tests/input/test_input_validation.py | 10 +++ tests/input/test_select_on_focus.py | 1 + tests/layouts/test_horizontal.py | 1 + tests/listview/test_inherit_listview.py | 3 + tests/listview/test_listview_initial_index.py | 1 + tests/listview/test_listview_navigation.py | 1 + tests/listview/test_listview_remove_items.py | 4 ++ .../test_all_levels_notifications.py | 1 + tests/notifications/test_app_notifications.py | 5 ++ tests/option_list/test_option_list_create.py | 12 ++++ .../option_list/test_option_list_disabled.py | 10 +++ .../test_option_list_id_stability.py | 1 + .../test_option_list_mouse_click.py | 1 + .../test_option_list_mouse_hover.py | 5 ++ .../option_list/test_option_list_movement.py | 16 +++++ .../test_option_list_option_subclass.py | 1 + tests/option_list/test_option_messages.py | 8 +++ .../test_option_prompt_replacement.py | 7 ++ tests/option_list/test_option_removal.py | 9 +++ tests/select/test_blank_and_clear.py | 5 ++ tests/select/test_changed_message.py | 4 ++ tests/select/test_empty_select.py | 4 ++ tests/select/test_prompt.py | 2 + tests/select/test_remove.py | 1 + tests/select/test_value.py | 9 +++ .../test_over_wide_selections.py | 1 + .../test_selection_click_checkbox.py | 2 + .../test_selection_list_create.py | 9 +++ .../selection_list/test_selection_messages.py | 13 ++++ tests/selection_list/test_selection_values.py | 8 +++ tests/suggester/test_input_suggestions.py | 9 +++ tests/suggester/test_suggest_from_list.py | 3 + tests/suggester/test_suggester.py | 7 ++ tests/test_animation.py | 10 +++ tests/test_animator.py | 3 + tests/test_app.py | 14 ++++ tests/test_app_focus_blur.py | 5 ++ tests/test_await_remove.py | 1 + tests/test_binding.py | 2 + tests/test_binding_inheritance.py | 15 ++++ tests/test_border_subtitle.py | 1 + tests/test_call_x_schedulers.py | 2 + tests/test_collapsible.py | 14 ++++ tests/test_command.py | 1 + tests/test_compositor.py | 1 + tests/test_containers.py | 5 ++ tests/test_content_switcher.py | 8 +++ tests/test_data_bind.py | 2 + tests/test_data_table.py | 72 +++++++++++++++++++ tests/test_demo.py | 1 + tests/test_disabled.py | 6 ++ tests/test_driver.py | 4 ++ tests/test_dynamic_bindings.py | 1 + tests/test_file_monitor.py | 1 + tests/test_focus.py | 19 +++++ tests/test_freeze.py | 1 + tests/test_gc.py | 1 + tests/test_header.py | 8 +++ tests/test_issue_4248.py | 1 + tests/test_keymap.py | 6 ++ tests/test_keys.py | 1 + tests/test_lazy.py | 2 + tests/test_links.py | 1 + tests/test_loading.py | 1 + tests/test_log.py | 1 + tests/test_markdown.py | 7 ++ tests/test_markdownviewer.py | 3 + tests/test_masked_input.py | 14 ++++ tests/test_message_handling.py | 1 + tests/test_message_pump.py | 7 ++ tests/test_modal.py | 1 + tests/test_mount.py | 1 + tests/test_on.py | 9 +++ tests/test_overflow_change.py | 1 + tests/test_paste.py | 2 + tests/test_pilot.py | 17 +++++ tests/test_progress_bar.py | 1 + tests/test_query.py | 8 +++ tests/test_reactive.py | 30 ++++++++ tests/test_rlock.py | 3 + tests/test_screen_modes.py | 10 +++ tests/test_screens.py | 19 +++++ tests/test_shutdown.py | 1 + tests/test_signal.py | 2 + tests/test_style_importance.py | 8 +++ tests/test_style_inheritance.py | 1 + tests/test_suspend.py | 2 + tests/test_switch.py | 1 + tests/test_tabbed_content.py | 40 +++++++++++ tests/test_tabs.py | 25 +++++++ tests/test_test_runner.py | 1 + tests/test_tooltips.py | 7 ++ tests/test_unmount.py | 1 + tests/test_visible.py | 2 + tests/test_widget.py | 27 +++++++ tests/test_widget_child_moving.py | 14 ++++ tests/test_widget_mounting.py | 2 + tests/test_widget_removing.py | 15 ++++ tests/test_widget_visibility.py | 1 + tests/text_area/test_edit_via_api.py | 28 ++++++++ tests/text_area/test_edit_via_bindings.py | 26 +++++++ tests/text_area/test_escape_binding.py | 2 + tests/text_area/test_history.py | 16 +++++ tests/text_area/test_issue_4301.py | 1 + tests/text_area/test_languages.py | 6 ++ tests/text_area/test_messages.py | 5 ++ tests/text_area/test_selection.py | 21 ++++++ tests/text_area/test_selection_bindings.py | 24 +++++++ tests/text_area/test_setting_themes.py | 4 ++ .../text_area/test_textarea_cut_copy_paste.py | 3 + tests/toggles/test_checkbox.py | 2 + tests/toggles/test_labels.py | 1 + tests/toggles/test_radiobutton.py | 2 + tests/toggles/test_radioset.py | 9 +++ tests/tree/test_directory_tree.py | 5 ++ tests/tree/test_node_refresh.py | 4 ++ tests/tree/test_tree_availability.py | 4 ++ tests/tree/test_tree_clearing.py | 7 ++ tests/tree/test_tree_cursor.py | 4 ++ tests/tree/test_tree_expand_etc.py | 6 ++ tests/tree/test_tree_messages.py | 7 ++ tests/workers/test_work_decorator.py | 4 ++ tests/workers/test_worker.py | 10 +++ tests/workers/test_worker_manager.py | 3 + 160 files changed, 999 insertions(+) diff --git a/docs/examples/guide/testing/test_rgb.py b/docs/examples/guide/testing/test_rgb.py index 030f62b505..dd4679aba8 100644 --- a/docs/examples/guide/testing/test_rgb.py +++ b/docs/examples/guide/testing/test_rgb.py @@ -3,6 +3,7 @@ from textual.color import Color +@pytest.mark.anyio async def test_keys(): # (1)! """Test pressing keys has the desired result.""" app = RGBApp() @@ -25,6 +26,7 @@ async def test_keys(): # (1)! assert app.screen.styles.background == Color.parse("blue") +@pytest.mark.anyio async def test_buttons(): """Test pressing keys has the desired result.""" app = RGBApp() diff --git a/tests/animations/test_disabling_animations.py b/tests/animations/test_disabling_animations.py index 2880262025..d60bdd25c9 100644 --- a/tests/animations/test_disabling_animations.py +++ b/tests/animations/test_disabling_animations.py @@ -20,6 +20,7 @@ def compose(self) -> ComposeResult: yield Label() +@pytest.mark.anyio async def test_style_animations_via_animate_work_on_full() -> None: app = SingleLabelApp() app.animation_level = "full" @@ -42,6 +43,7 @@ async def test_style_animations_via_animate_work_on_full() -> None: assert label.styles.background != Color.parse("blue") +@pytest.mark.anyio async def test_style_animations_via_animate_are_disabled_on_basic() -> None: app = SingleLabelApp() app.animation_level = "basic" @@ -63,6 +65,7 @@ async def test_style_animations_via_animate_are_disabled_on_basic() -> None: assert label.styles.background == Color.parse("blue") +@pytest.mark.anyio async def test_style_animations_via_animate_are_disabled_on_none() -> None: app = SingleLabelApp() app.animation_level = "none" @@ -102,6 +105,7 @@ def compose(self) -> ComposeResult: yield Label() +@pytest.mark.anyio async def test_style_animations_via_transition_work_on_full() -> None: app = LabelWithTransitionsApp() app.animation_level = "full" @@ -123,6 +127,7 @@ async def test_style_animations_via_transition_work_on_full() -> None: assert label.styles.background != Color.parse("blue") +@pytest.mark.anyio async def test_style_animations_via_transition_are_disabled_on_basic() -> None: app = LabelWithTransitionsApp() app.animation_level = "basic" @@ -143,6 +148,7 @@ async def test_style_animations_via_transition_are_disabled_on_basic() -> None: assert label.styles.background == Color.parse("blue") +@pytest.mark.anyio async def test_style_animations_via_transition_are_disabled_on_none() -> None: app = LabelWithTransitionsApp() app.animation_level = "none" diff --git a/tests/animations/test_loading_indicator_animation.py b/tests/animations/test_loading_indicator_animation.py index 38bb6a7c27..0be388d620 100644 --- a/tests/animations/test_loading_indicator_animation.py +++ b/tests/animations/test_loading_indicator_animation.py @@ -6,6 +6,7 @@ from textual.app import App +@pytest.mark.anyio async def test_loading_indicator_is_not_static_on_full() -> None: """The loading indicator doesn't fall back to the static render on FULL.""" app = App() @@ -18,6 +19,7 @@ async def test_loading_indicator_is_not_static_on_full() -> None: assert str(indicator.render()) != "Loading..." +@pytest.mark.anyio async def test_loading_indicator_is_not_static_on_basic() -> None: """The loading indicator doesn't fall back to the static render on BASIC.""" app = App() @@ -30,6 +32,7 @@ async def test_loading_indicator_is_not_static_on_basic() -> None: assert str(indicator.render()) != "Loading..." +@pytest.mark.anyio async def test_loading_indicator_is_static_on_none() -> None: """The loading indicator falls back to the static render on NONE.""" app = App() diff --git a/tests/animations/test_progress_bar_animation.py b/tests/animations/test_progress_bar_animation.py index 82e2add599..24957cab6f 100644 --- a/tests/animations/test_progress_bar_animation.py +++ b/tests/animations/test_progress_bar_animation.py @@ -13,6 +13,7 @@ def compose(self) -> ComposeResult: yield ProgressBar() +@pytest.mark.anyio async def test_progress_bar_animates_on_full() -> None: """An indeterminate progress bar is not fully highlighted when animating.""" app = ProgressBarApp() @@ -24,6 +25,7 @@ async def test_progress_bar_animates_on_full() -> None: assert start != 0 or end != app.query_one(Bar).size.width +@pytest.mark.anyio async def test_progress_bar_animates_on_basic() -> None: """An indeterminate progress bar is not fully highlighted when animating.""" app = ProgressBarApp() @@ -35,6 +37,7 @@ async def test_progress_bar_animates_on_basic() -> None: assert start != 0 or end != app.query_one(Bar).size.width +@pytest.mark.anyio async def test_progress_bar_does_not_animate_on_none() -> None: """An indeterminate progress bar is fully highlighted when not animating.""" app = ProgressBarApp() diff --git a/tests/animations/test_scrolling_animation.py b/tests/animations/test_scrolling_animation.py index 172dc09c61..b77d5f8aaf 100644 --- a/tests/animations/test_scrolling_animation.py +++ b/tests/animations/test_scrolling_animation.py @@ -15,6 +15,7 @@ def compose(self) -> ComposeResult: yield Label() +@pytest.mark.anyio async def test_scrolling_animates_on_full() -> None: app = TallApp() app.animation_level = "full" @@ -33,6 +34,7 @@ async def test_scrolling_animates_on_full() -> None: assert animator.is_being_animated(vertical_scroll, "scroll_y") +@pytest.mark.anyio async def test_scrolling_animates_on_basic() -> None: app = TallApp() app.animation_level = "basic" @@ -51,6 +53,7 @@ async def test_scrolling_animates_on_basic() -> None: assert animator.is_being_animated(vertical_scroll, "scroll_y") +@pytest.mark.anyio async def test_scrolling_does_not_animate_on_none() -> None: app = TallApp() app.animation_level = "none" diff --git a/tests/animations/test_switch_animation.py b/tests/animations/test_switch_animation.py index 832ea9cb99..f5b8cda8b3 100644 --- a/tests/animations/test_switch_animation.py +++ b/tests/animations/test_switch_animation.py @@ -12,6 +12,7 @@ def compose(self) -> ComposeResult: yield Switch() +@pytest.mark.anyio async def test_switch_animates_on_full() -> None: app = SwitchApp() app.animation_level = "full" @@ -31,6 +32,7 @@ async def test_switch_animates_on_full() -> None: assert app.animator.is_being_animated(switch, "_slider_position") +@pytest.mark.anyio async def test_switch_animates_on_basic() -> None: app = SwitchApp() app.animation_level = "basic" @@ -50,6 +52,7 @@ async def test_switch_animates_on_basic() -> None: assert app.animator.is_being_animated(switch, "_slider_position") +@pytest.mark.anyio async def test_switch_does_not_animate_on_none() -> None: app = SwitchApp() app.animation_level = "none" diff --git a/tests/animations/test_tabs_underline_animation.py b/tests/animations/test_tabs_underline_animation.py index c50409d59a..07df0b7fcd 100644 --- a/tests/animations/test_tabs_underline_animation.py +++ b/tests/animations/test_tabs_underline_animation.py @@ -14,6 +14,7 @@ def compose(self) -> ComposeResult: yield Label("Hey!") +@pytest.mark.anyio async def test_tabs_underline_animates_on_full() -> None: """The underline takes some time to move when animated.""" app = TabbedContentApp() @@ -30,6 +31,7 @@ async def test_tabs_underline_animates_on_full() -> None: assert "highlight_end" in animations +@pytest.mark.anyio async def test_tabs_underline_animates_on_basic() -> None: """The underline takes some time to move when animated.""" app = TabbedContentApp() @@ -46,6 +48,7 @@ async def test_tabs_underline_animates_on_basic() -> None: assert "highlight_end" in animations +@pytest.mark.anyio async def test_tabs_underline_does_not_animate_on_none() -> None: """The underline jumps to its final position when not animated.""" app = TabbedContentApp() diff --git a/tests/command_palette/test_click_away.py b/tests/command_palette/test_click_away.py index ad8f989113..e4ce749fb4 100644 --- a/tests/command_palette/test_click_away.py +++ b/tests/command_palette/test_click_away.py @@ -17,6 +17,7 @@ def on_mount(self) -> None: self.action_command_palette() +@pytest.mark.anyio async def test_clicking_outside_command_palette_closes_it() -> None: """Clicking 'outside' the command palette should make it go away.""" async with CommandPaletteApp().run_test() as pilot: diff --git a/tests/command_palette/test_command_source_environment.py b/tests/command_palette/test_command_source_environment.py index 29145cdf04..bd299c0bff 100644 --- a/tests/command_palette/test_command_source_environment.py +++ b/tests/command_palette/test_command_source_environment.py @@ -28,6 +28,7 @@ def on_mount(self) -> None: self.action_command_palette() +@pytest.mark.anyio async def test_command_source_environment() -> None: """The command source should see the app and default screen.""" async with CommandPaletteApp().run_test() as pilot: diff --git a/tests/command_palette/test_declare_sources.py b/tests/command_palette/test_declare_sources.py index e7691476be..724887d93d 100644 --- a/tests/command_palette/test_declare_sources.py +++ b/tests/command_palette/test_declare_sources.py @@ -4,6 +4,7 @@ from textual.system_commands import SystemCommandsProvider +@pytest.mark.anyio async def test_sources_with_no_known_screen() -> None: """A command palette with no known screen should have an empty source set.""" assert CommandPalette()._provider_classes == set() @@ -26,6 +27,7 @@ class AppWithNoSources(AppWithActiveCommandPalette): pass +@pytest.mark.anyio async def test_no_app_command_sources() -> None: """An app with no sources declared should work fine.""" async with AppWithNoSources().run_test() as pilot: @@ -37,6 +39,7 @@ class AppWithSources(AppWithActiveCommandPalette): COMMANDS = {ExampleCommandSource} +@pytest.mark.anyio async def test_app_command_sources() -> None: """Command sources declared on an app should be in the command palette.""" async with AppWithSources().run_test() as pilot: @@ -58,6 +61,7 @@ def on_mount(self) -> None: self.app.action_command_palette() +@pytest.mark.anyio async def test_no_screen_command_sources() -> None: """An app with a screen with no sources declared should work fine.""" async with AppWithInitialScreen(ScreenWithNoSources()).run_test() as pilot: @@ -69,6 +73,7 @@ class ScreenWithSources(ScreenWithNoSources): COMMANDS = {ExampleCommandSource} +@pytest.mark.anyio async def test_screen_command_sources() -> None: """Command sources declared on a screen should be in the command palette.""" async with AppWithInitialScreen(ScreenWithSources()).run_test() as pilot: @@ -90,6 +95,7 @@ def on_mount(self) -> None: self.push_screen(ScreenWithSources()) +@pytest.mark.anyio async def test_app_and_screen_command_sources_combine() -> None: """If an app and the screen have command sources they should combine.""" async with CombinedSourceApp().run_test() as pilot: diff --git a/tests/command_palette/test_discover.py b/tests/command_palette/test_discover.py index 24849adf07..c506c5689c 100644 --- a/tests/command_palette/test_discover.py +++ b/tests/command_palette/test_discover.py @@ -25,6 +25,7 @@ def on_mount(self) -> None: self.action_command_palette() +@pytest.mark.anyio async def test_discovery_visible() -> None: """A provider with discovery should cause the command palette to be opened right away.""" async with CommandPaletteApp().run_test() as pilot: diff --git a/tests/command_palette/test_escaping.py b/tests/command_palette/test_escaping.py index 11fb1ded87..23ccc5249d 100644 --- a/tests/command_palette/test_escaping.py +++ b/tests/command_palette/test_escaping.py @@ -17,6 +17,7 @@ def on_mount(self) -> None: self.action_command_palette() +@pytest.mark.anyio async def test_escape_closes_when_no_list_visible() -> None: """Pressing escape when no list is visible should close the command palette.""" async with CommandPaletteApp().run_test() as pilot: diff --git a/tests/command_palette/test_events.py b/tests/command_palette/test_events.py index 47f5b8eee1..4aebb9b0cf 100644 --- a/tests/command_palette/test_events.py +++ b/tests/command_palette/test_events.py @@ -38,12 +38,14 @@ def record_event( self.events.append(event) +@pytest.mark.anyio async def test_command_palette_opened_event(): app = AppWithActiveCommandPalette() async with app.run_test(): assert app.events == [CommandPalette.Opened()] +@pytest.mark.anyio async def test_command_palette_closed_event(): app = AppWithActiveCommandPalette() async with app.run_test() as pilot: @@ -51,6 +53,7 @@ async def test_command_palette_closed_event(): assert app.events == [CommandPalette.Opened(), CommandPalette.Closed(False)] +@pytest.mark.anyio async def test_command_palette_closed_event_value(): app = AppWithActiveCommandPalette() async with app.run_test() as pilot: @@ -64,6 +67,7 @@ async def test_command_palette_closed_event_value(): ] +@pytest.mark.anyio async def test_command_palette_option_highlighted_event(): app = AppWithActiveCommandPalette() async with app.run_test() as pilot: diff --git a/tests/command_palette/test_interaction.py b/tests/command_palette/test_interaction.py index b7e850189c..5dc93db099 100644 --- a/tests/command_palette/test_interaction.py +++ b/tests/command_palette/test_interaction.py @@ -18,6 +18,7 @@ def on_mount(self) -> None: self.action_command_palette() +@pytest.mark.anyio async def test_initial_list_no_highlight() -> None: """When the list initially appears, the first item is highlghted.""" async with CommandPaletteApp().run_test() as pilot: @@ -28,6 +29,7 @@ async def test_initial_list_no_highlight() -> None: assert pilot.app.screen.query_one(CommandList).highlighted == 0 +@pytest.mark.anyio async def test_down_arrow_selects_an_item() -> None: """Typing in a search value then pressing down should select a command.""" async with CommandPaletteApp().run_test() as pilot: @@ -40,6 +42,7 @@ async def test_down_arrow_selects_an_item() -> None: assert pilot.app.screen.query_one(CommandList).highlighted == 1 +@pytest.mark.anyio async def test_enter_selects_an_item() -> None: """Typing in a search value then pressing enter should dismiss the command palette.""" async with CommandPaletteApp().run_test() as pilot: diff --git a/tests/command_palette/test_no_results.py b/tests/command_palette/test_no_results.py index 8328853b83..f09a4efc00 100644 --- a/tests/command_palette/test_no_results.py +++ b/tests/command_palette/test_no_results.py @@ -10,6 +10,7 @@ def on_mount(self) -> None: self.action_command_palette() +@pytest.mark.anyio async def test_no_results() -> None: """Receiving no results from a search for a command should not be a problem.""" async with CommandPaletteApp().run_test() as pilot: diff --git a/tests/command_palette/test_run_on_select.py b/tests/command_palette/test_run_on_select.py index 31946a127b..99e7167bc3 100644 --- a/tests/command_palette/test_run_on_select.py +++ b/tests/command_palette/test_run_on_select.py @@ -29,6 +29,7 @@ def __init__(self) -> None: self.selection: int | None = None +@pytest.mark.anyio async def test_with_run_on_select_on() -> None: """With run on select on, the callable should be instantly run.""" async with CommandPaletteRunOnSelectApp().run_test() as pilot: @@ -49,6 +50,7 @@ def __init__(self) -> None: super().__init__() +@pytest.mark.anyio async def test_with_run_on_select_off() -> None: """With run on select off, the callable should not be instantly run.""" async with CommandPaletteDoNotRunOnSelectApp().run_test() as pilot: diff --git a/tests/command_palette/test_worker_interference.py b/tests/command_palette/test_worker_interference.py index b259d748e5..3fa0624b73 100644 --- a/tests/command_palette/test_worker_interference.py +++ b/tests/command_palette/test_worker_interference.py @@ -20,6 +20,7 @@ class CommandPaletteNoWorkerApp(App[None]): COMMANDS = {SimpleSource} +@pytest.mark.anyio async def test_no_command_palette_worker_droppings() -> None: """The command palette should not leave any workers behind..""" async with CommandPaletteNoWorkerApp().run_test() as pilot: @@ -41,6 +42,7 @@ async def innocent_worker(self) -> None: await sleep(1) +@pytest.mark.anyio async def test_innocent_worker_is_untouched() -> None: """Using the command palette should not halt other workers.""" async with CommandPaletteWithWorkerApp().run_test() as pilot: diff --git a/tests/css/test_css_reloading.py b/tests/css/test_css_reloading.py index b572e97dcd..1a29f80fe8 100644 --- a/tests/css/test_css_reloading.py +++ b/tests/css/test_css_reloading.py @@ -30,6 +30,7 @@ def on_mount(self) -> None: self.push_screen(TopScreen()) +@pytest.mark.anyio async def test_css_reloading_applies_to_non_top_screen(monkeypatch) -> None: # type: ignore """Regression test for https://github.com/Textualize/textual/issues/3931""" @@ -64,6 +65,7 @@ async def test_css_reloading_applies_to_non_top_screen(monkeypatch) -> None: # assert first_label.styles.height.value == 1 +@pytest.mark.anyio async def test_css_reloading_file_not_found(monkeypatch, tmp_path): """Regression test for https://github.com/Textualize/textual/issues/3996 diff --git a/tests/css/test_inheritance.py b/tests/css/test_inheritance.py index 2dd6f25a4e..be78d956ba 100644 --- a/tests/css/test_inheritance.py +++ b/tests/css/test_inheritance.py @@ -25,6 +25,7 @@ class Widget2(Widget1): @pytest.mark.xfail( reason="Overlapping styles should prioritize the most recent widget in the inheritance chain" ) +@pytest.mark.anyio async def test_inheritance(): class InheritanceApp(App): def compose(self) -> ComposeResult: diff --git a/tests/css/test_initial.py b/tests/css/test_initial.py index a0b880c479..bbfc168a54 100644 --- a/tests/css/test_initial.py +++ b/tests/css/test_initial.py @@ -31,6 +31,7 @@ class CustomWidget3(CustomWidget2): pass +@pytest.mark.anyio async def test_initial_default(): class InitialApp(App): def compose(self) -> ComposeResult: @@ -53,6 +54,7 @@ def compose(self) -> ComposeResult: assert custom2.styles.background == default_background +@pytest.mark.anyio async def test_initial(): class InitialApp(App): CSS = """ diff --git a/tests/css/test_nested_css.py b/tests/css/test_nested_css.py index 44f1236f55..96eacdafd0 100644 --- a/tests/css/test_nested_css.py +++ b/tests/css/test_nested_css.py @@ -34,6 +34,7 @@ def compose(self) -> ComposeResult: yield Label("World", classes="paul") +@pytest.mark.anyio async def test_nest_app(): """Test nested CSS works as expected.""" app = NestedApp() @@ -59,6 +60,7 @@ def compose(self) -> ComposeResult: yield Label("three", classes="heh") +@pytest.mark.anyio async def test_lists_of_selectors_in_nested_css() -> None: """Regression test for https://github.com/Textualize/textual/issues/3969.""" app = ListOfNestedSelectorsApp() @@ -83,6 +85,7 @@ def compose(self) -> ComposeResult: yield Label("one") +@pytest.mark.anyio async def test_rule_declaration_after_nested() -> None: """Regression test for https://github.com/Textualize/textual/issues/3999.""" app = DeclarationAfterNestedApp() @@ -157,6 +160,7 @@ def compose(self) -> ComposeResult: yield Label("Hello, world!", id="eight", classes="second_half") +@pytest.mark.anyio async def test_pseudo_classes_work_in_nested_css() -> None: """Makes sure pseudo-classes are correctly understood in nested TCSS. diff --git a/tests/css/test_programmatic_style_changes.py b/tests/css/test_programmatic_style_changes.py index c81d88fa31..333a6eda49 100644 --- a/tests/css/test_programmatic_style_changes.py +++ b/tests/css/test_programmatic_style_changes.py @@ -16,6 +16,7 @@ ("grid_columns", "1fr 3fr"), ], ) +@pytest.mark.anyio async def test_programmatic_style_change_updates_children(style: str, value: object): """Regression test for #1607 https://github.com/Textualize/textual/issues/1607 @@ -62,6 +63,7 @@ def compose(self): ("align", ("right", "bottom")), ], ) +@pytest.mark.anyio async def test_programmatic_align_change_updates_children_position( style: str, value: str ): diff --git a/tests/css/test_screen_css.py b/tests/css/test_screen_css.py index a39e265cae..524c2b4b74 100644 --- a/tests/css/test_screen_css.py +++ b/tests/css/test_screen_css.py @@ -69,6 +69,7 @@ def check_colors_after_screen_css(app: BaseApp): assert app.screen.query_one("#screen-css").styles.background == RED +@pytest.mark.anyio async def test_screen_pushing_and_popping_does_not_reparse_css(): """Check that pushing and popping the same screen doesn't trigger CSS reparses.""" @@ -103,6 +104,7 @@ def _reparse(*args, **kwargs): assert counter == 1 +@pytest.mark.anyio async def test_screen_css_push_screen_instance(): """Check that screen CSS is loaded and applied when pushing a screen instance.""" @@ -122,6 +124,7 @@ def key_o(self): check_colors_after_screen_css(app) +@pytest.mark.anyio async def test_screen_css_push_screen_instance_by_name(): """Check that screen CSS is loaded and applied when pushing a screen name that points to a screen instance.""" @@ -143,6 +146,7 @@ def key_o(self): check_colors_after_screen_css(app) +@pytest.mark.anyio async def test_screen_css_push_screen_type_by_name(): """Check that screen CSS is loaded and applied when pushing a screen name that points to a screen class.""" @@ -164,6 +168,7 @@ def key_o(self): check_colors_after_screen_css(app) +@pytest.mark.anyio async def test_screen_css_switch_screen_instance(): """Check that screen CSS is loaded and applied when switching to a screen instance.""" @@ -183,6 +188,7 @@ def key_o(self): check_colors_after_screen_css(app) +@pytest.mark.anyio async def test_screen_css_switch_screen_instance_by_name(): """Check that screen CSS is loaded and applied when switching a screen name that points to a screen instance.""" @@ -204,6 +210,7 @@ def key_o(self): check_colors_after_screen_css(app) +@pytest.mark.anyio async def test_screen_css_switch_screen_type_by_name(): """Check that screen CSS is loaded and applied when switching a screen name that points to a screen class.""" @@ -225,6 +232,7 @@ def key_o(self): check_colors_after_screen_css(app) +@pytest.mark.anyio async def test_screen_css_switch_mode_screen_instance(): """Check that screen CSS is loaded and applied when switching to a mode with a screen instance.""" @@ -250,6 +258,7 @@ def key_o(self): check_colors_after_screen_css(app) +@pytest.mark.anyio async def test_screen_css_switch_mode_screen_instance_by_name(): """Check that screen CSS is loaded and applied when switching to a mode with a screen instance name.""" @@ -279,6 +288,7 @@ def key_o(self): check_colors_after_screen_css(app) +@pytest.mark.anyio async def test_screen_css_switch_mode_screen_type_by_name(): """Check that screen CSS is loaded and applied when switching to a mode with a screen type name.""" diff --git a/tests/directory_tree/test_change_path.py b/tests/directory_tree/test_change_path.py index 1b73f3c09d..b56e085f7c 100644 --- a/tests/directory_tree/test_change_path.py +++ b/tests/directory_tree/test_change_path.py @@ -10,6 +10,7 @@ def compose(self) -> ComposeResult: yield DirectoryTree(".") +@pytest.mark.anyio async def test_change_directory_tree_path(tmpdir: Path) -> None: """The DirectoryTree should react to the path changing.""" diff --git a/tests/directory_tree/test_early_show_root.py b/tests/directory_tree/test_early_show_root.py index 73e1995337..7f85c30143 100644 --- a/tests/directory_tree/test_early_show_root.py +++ b/tests/directory_tree/test_early_show_root.py @@ -9,6 +9,7 @@ def compose(self) -> ComposeResult: yield tree +@pytest.mark.anyio async def test_managed_to_set_show_root_before_mounted() -> None: """https://github.com/Textualize/textual/issues/2363""" async with DirectoryTreeApp().run_test() as pilot: diff --git a/tests/footer/test_footer.py b/tests/footer/test_footer.py index 2016451c04..291b1a54c1 100644 --- a/tests/footer/test_footer.py +++ b/tests/footer/test_footer.py @@ -4,6 +4,7 @@ from textual.widgets import Button, Footer +@pytest.mark.anyio async def test_footer_bindings() -> None: app_binding_count = 0 diff --git a/tests/input/test_cut_copy_paste.py b/tests/input/test_cut_copy_paste.py index b6dd434e14..3dada767f2 100644 --- a/tests/input/test_cut_copy_paste.py +++ b/tests/input/test_cut_copy_paste.py @@ -7,6 +7,7 @@ def compose(self) -> ComposeResult: yield Input() +@pytest.mark.anyio async def test_cut(): """Check that cut removes text and places it in the clipboard.""" app = InputApp() @@ -20,6 +21,7 @@ async def test_cut(): assert app.clipboard == "rl" +@pytest.mark.anyio async def test_copy(): """Check that copy places text in the clipboard.""" app = InputApp() @@ -33,6 +35,7 @@ async def test_copy(): assert app.clipboard == "rl" +@pytest.mark.anyio async def test_paste(): """Check that paste copies text from the local clipboard.""" app = InputApp() diff --git a/tests/input/test_input_clear.py b/tests/input/test_input_clear.py index 07abff1a76..b157dcaf5d 100644 --- a/tests/input/test_input_clear.py +++ b/tests/input/test_input_clear.py @@ -7,6 +7,7 @@ def compose(self) -> ComposeResult: yield Input("Hello, World!") +@pytest.mark.anyio async def test_input_clear(): async with InputApp().run_test() as pilot: input_widget = pilot.app.query_one(Input) diff --git a/tests/input/test_input_key_modification_actions.py b/tests/input/test_input_key_modification_actions.py index 187fe5e8b1..8cfc227135 100644 --- a/tests/input/test_input_key_modification_actions.py +++ b/tests/input/test_input_key_modification_actions.py @@ -21,6 +21,7 @@ def compose(self) -> ComposeResult: yield Input(value, id=input_id) +@pytest.mark.anyio async def test_delete_left_from_home() -> None: """Deleting left from home should do nothing.""" async with InputTester().run_test() as pilot: @@ -31,6 +32,7 @@ async def test_delete_left_from_home() -> None: assert input.value == TEST_INPUTS[input.id] +@pytest.mark.anyio async def test_delete_left_from_end() -> None: """Deleting left from end should remove the last character (if there is one).""" async with InputTester().run_test() as pilot: @@ -41,6 +43,7 @@ async def test_delete_left_from_end() -> None: assert input.value == TEST_INPUTS[input.id][:-1] +@pytest.mark.anyio async def test_delete_left_word_from_home() -> None: """Deleting word left from home should do nothing.""" async with InputTester().run_test() as pilot: @@ -51,6 +54,7 @@ async def test_delete_left_word_from_home() -> None: assert input.value == TEST_INPUTS[input.id] +@pytest.mark.anyio async def test_delete_left_word_from_inside_first_word() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): @@ -60,6 +64,7 @@ async def test_delete_left_word_from_inside_first_word() -> None: assert input.value == TEST_INPUTS[input.id][1:] +@pytest.mark.anyio async def test_delete_left_word_from_end() -> None: """Deleting word left from end should remove the expected text.""" async with InputTester().run_test() as pilot: @@ -75,6 +80,7 @@ async def test_delete_left_word_from_end() -> None: assert input.value == expected[input.id] +@pytest.mark.anyio async def test_password_delete_left_word_from_end() -> None: """Deleting word left from end of a password input should delete everything.""" async with InputTester().run_test() as pilot: @@ -85,6 +91,7 @@ async def test_password_delete_left_word_from_end() -> None: assert input.value == "" +@pytest.mark.anyio async def test_delete_left_all_from_home() -> None: """Deleting all left from home should do nothing.""" async with InputTester().run_test() as pilot: @@ -95,6 +102,7 @@ async def test_delete_left_all_from_home() -> None: assert input.value == TEST_INPUTS[input.id] +@pytest.mark.anyio async def test_delete_left_all_from_end() -> None: """Deleting all left from end should empty the input value.""" async with InputTester().run_test() as pilot: @@ -105,6 +113,7 @@ async def test_delete_left_all_from_end() -> None: assert input.value == "" +@pytest.mark.anyio async def test_delete_right_from_home() -> None: """Deleting right from home should delete one character (if there is any to delete).""" async with InputTester().run_test() as pilot: @@ -115,6 +124,7 @@ async def test_delete_right_from_home() -> None: assert input.value == TEST_INPUTS[input.id][1:] +@pytest.mark.anyio async def test_delete_right_from_end() -> None: """Deleting right from end should not change the input's value.""" async with InputTester().run_test() as pilot: @@ -124,6 +134,7 @@ async def test_delete_right_from_end() -> None: assert input.value == TEST_INPUTS[input.id] +@pytest.mark.anyio async def test_delete_right_word_from_home() -> None: """Deleting word right from home should delete one word (if there is one).""" async with InputTester().run_test() as pilot: @@ -140,6 +151,7 @@ async def test_delete_right_word_from_home() -> None: assert input.value == expected[input.id] +@pytest.mark.anyio async def test_password_delete_right_word_from_home() -> None: """Deleting word right from home of a password input should delete everything.""" async with InputTester().run_test() as pilot: @@ -151,6 +163,7 @@ async def test_password_delete_right_word_from_home() -> None: assert input.value == "" +@pytest.mark.anyio async def test_delete_right_word_from_end() -> None: """Deleting word right from end should not change the input's value.""" async with InputTester().run_test() as pilot: @@ -161,6 +174,7 @@ async def test_delete_right_word_from_end() -> None: assert input.value == TEST_INPUTS[input.id] +@pytest.mark.anyio async def test_delete_right_all_from_home() -> None: """Deleting all right home should remove everything in the input.""" async with InputTester().run_test() as pilot: @@ -171,6 +185,7 @@ async def test_delete_right_all_from_home() -> None: assert input.value == "" +@pytest.mark.anyio async def test_delete_right_all_from_end() -> None: """Deleting all right from end should not change the input's value.""" async with InputTester().run_test() as pilot: diff --git a/tests/input/test_input_key_movement_actions.py b/tests/input/test_input_key_movement_actions.py index b47befc6eb..4c1bf72b59 100644 --- a/tests/input/test_input_key_movement_actions.py +++ b/tests/input/test_input_key_movement_actions.py @@ -23,6 +23,7 @@ def compose(self) -> ComposeResult: yield Input(value, id=input_id) +@pytest.mark.anyio async def test_input_home() -> None: """Going home should always land at position zero.""" async with InputTester().run_test() as pilot: @@ -31,6 +32,7 @@ async def test_input_home() -> None: assert input.cursor_position == 0 +@pytest.mark.anyio async def test_input_end() -> None: """Going end should always land at the last position.""" async with InputTester().run_test() as pilot: @@ -39,6 +41,7 @@ async def test_input_end() -> None: assert input.cursor_position == len(input.value) +@pytest.mark.anyio async def test_input_right_from_home() -> None: """Going right should always land at the next position, if there is one.""" async with InputTester().run_test() as pilot: @@ -48,6 +51,7 @@ async def test_input_right_from_home() -> None: assert input.cursor_position == (1 if input.value else 0) +@pytest.mark.anyio async def test_input_right_from_end() -> None: """Going right should always stay put if doing so from the end.""" async with InputTester().run_test() as pilot: @@ -57,6 +61,7 @@ async def test_input_right_from_end() -> None: assert input.cursor_position == len(input.value) +@pytest.mark.anyio async def test_input_left_from_home() -> None: """Going left from home should stay put.""" async with InputTester().run_test() as pilot: @@ -66,6 +71,7 @@ async def test_input_left_from_home() -> None: assert input.cursor_position == 0 +@pytest.mark.anyio async def test_input_left_from_end() -> None: """Going left from the end should go back one place, where possible.""" async with InputTester().run_test() as pilot: @@ -75,6 +81,7 @@ async def test_input_left_from_end() -> None: assert input.cursor_position == (len(input.value) - 1 if input.value else 0) +@pytest.mark.anyio async def test_input_left_word_from_home() -> None: """Going left one word from the start should do nothing.""" async with InputTester().run_test() as pilot: @@ -84,6 +91,7 @@ async def test_input_left_word_from_home() -> None: assert input.cursor_position == 0 +@pytest.mark.anyio async def test_input_left_word_from_end() -> None: """Going left one word from the end should land correctly..""" async with InputTester().run_test() as pilot: @@ -100,6 +108,7 @@ async def test_input_left_word_from_end() -> None: assert input.cursor_position == expected_at[input.id] +@pytest.mark.anyio async def test_password_input_left_word_from_end() -> None: """Going left one word from the end in a password field should land at home.""" async with InputTester().run_test() as pilot: @@ -110,6 +119,7 @@ async def test_password_input_left_word_from_end() -> None: assert input.cursor_position == 0 +@pytest.mark.anyio async def test_input_right_word_from_home() -> None: """Going right one word from the start should land correctly..""" async with InputTester().run_test() as pilot: @@ -126,6 +136,7 @@ async def test_input_right_word_from_home() -> None: assert input.cursor_position == expected_at[input.id] +@pytest.mark.anyio async def test_password_input_right_word_from_home() -> None: """Going right one word from the start of a password input should go to the end.""" async with InputTester().run_test() as pilot: @@ -135,6 +146,7 @@ async def test_password_input_right_word_from_home() -> None: assert input.cursor_position == len(input.value) +@pytest.mark.anyio async def test_input_right_word_from_end() -> None: """Going right one word from the end should do nothing.""" async with InputTester().run_test() as pilot: @@ -144,6 +156,7 @@ async def test_input_right_word_from_end() -> None: assert input.cursor_position == len(input.value) +@pytest.mark.anyio async def test_input_right_word_to_the_end() -> None: """Using right-word to get to the end should hop the correct number of times.""" async with InputTester().run_test() as pilot: @@ -163,6 +176,7 @@ async def test_input_right_word_to_the_end() -> None: assert hops == expected_hops[input.id] +@pytest.mark.anyio async def test_input_left_word_from_the_end() -> None: """Using left-word to get home from the end should hop the correct number of times.""" async with InputTester().run_test() as pilot: diff --git a/tests/input/test_input_messages.py b/tests/input/test_input_messages.py index bbfa7bfcc4..5acf7560f2 100644 --- a/tests/input/test_input_messages.py +++ b/tests/input/test_input_messages.py @@ -25,18 +25,21 @@ def log_message(self, event: Input.Submitted | Input.Changed) -> None: self.messages.append(event.__class__.__name__) +@pytest.mark.anyio async def test_no_startup_messages(): """An input with no initial value should have no initial messages.""" async with InputApp().run_test() as pilot: assert pilot.app.messages == [] +@pytest.mark.anyio async def test_startup_messages_with_initial_value(): """An input with an initial value should send a changed event.""" async with InputApp("Hello, World!").run_test() as pilot: assert pilot.app.messages == ["Changed"] +@pytest.mark.anyio async def test_typing_from_empty_causes_changed(): """An input with no initial value should send messages when entering text.""" input_text = "Hello, World!" @@ -45,6 +48,7 @@ async def test_typing_from_empty_causes_changed(): assert pilot.app.messages == ["Changed"] * len(input_text) +@pytest.mark.anyio async def test_typing_from_pre_populated_causes_changed(): """An input with initial value should send messages when entering text after an initial message.""" input_text = "Hello, World!" @@ -53,6 +57,7 @@ async def test_typing_from_pre_populated_causes_changed(): assert pilot.app.messages == ["Changed"] + (["Changed"] * len(input_text)) +@pytest.mark.anyio async def test_submit_empty_input(): """Pressing enter on an empty input should send a submitted event.""" async with InputApp().run_test() as pilot: @@ -60,6 +65,7 @@ async def test_submit_empty_input(): assert pilot.app.messages == ["Submitted"] +@pytest.mark.anyio async def test_submit_pre_populated_input(): """Pressing enter on a pre-populated input should send a changed then submitted event.""" async with InputApp("The owls are not what they seem").run_test() as pilot: @@ -67,6 +73,7 @@ async def test_submit_pre_populated_input(): assert pilot.app.messages == ["Changed", "Submitted"] +@pytest.mark.anyio async def test_paste_event_impact(): """A paste event should result in a changed event.""" async with InputApp().run_test() as pilot: diff --git a/tests/input/test_input_mouse.py b/tests/input/test_input_mouse.py index 9793e21eac..bc22849da5 100644 --- a/tests/input/test_input_mouse.py +++ b/tests/input/test_input_mouse.py @@ -58,6 +58,7 @@ def compose(self) -> ComposeResult: (TEXT_MIXED, 60, 10), ), ) +@pytest.mark.anyio async def test_mouse_clicks_within(text, click_at, should_land): """Mouse clicks should result in the cursor going to the right place.""" async with InputApp(text).run_test() as pilot: @@ -68,6 +69,7 @@ async def test_mouse_clicks_within(text, click_at, should_land): assert pilot.app.query_one(Input).cursor_position == should_land +@pytest.mark.anyio async def test_mouse_click_outwith_moves_cursor_to_nearest_cell(): """Mouse clicks in the padding or border area should move the cursor as this makes dragging and selecting text easier.""" diff --git a/tests/input/test_input_properties.py b/tests/input/test_input_properties.py index ef518ba69d..7f249f6f81 100644 --- a/tests/input/test_input_properties.py +++ b/tests/input/test_input_properties.py @@ -23,12 +23,14 @@ def compose(self) -> ComposeResult: yield Input(self.TEST_TEXT) +@pytest.mark.anyio async def test_internal_value_no_password(): """The displayed value should be the input value.""" async with InputApp().run_test() as pilot: assert pilot.app.query_one(Input)._value == Text(pilot.app.TEST_TEXT) +@pytest.mark.anyio async def test_internal_value_password(): """The displayed value should be a password text.""" async with InputApp().run_test() as pilot: @@ -36,6 +38,7 @@ async def test_internal_value_password(): assert pilot.app.query_one(Input)._value == Text("•" * len(pilot.app.TEST_TEXT)) +@pytest.mark.anyio async def test_internal_value_highlighted(): async with InputApp().run_test() as pilot: pilot.app.query_one(Input).highlighter = JSONHighlighter() @@ -44,6 +47,7 @@ async def test_internal_value_highlighted(): assert pilot.app.query_one(Input)._value == JSONHighlighter()(test_text) +@pytest.mark.anyio async def test_cursor_toggle(): """Cursor toggling should toggle the cursor.""" async with InputApp().run_test() as pilot: @@ -54,6 +58,7 @@ async def test_cursor_toggle(): assert input_widget._cursor_visible is False +@pytest.mark.anyio async def test_input_height(): """Height should be 1 even if set to auto.""" async with InputApp().run_test() as pilot: @@ -64,6 +69,7 @@ async def test_input_height(): assert input_widget.parent.styles.height.value == 1 +@pytest.mark.anyio async def test_input_selected_text(): async with InputApp().run_test() as pilot: input_widget = pilot.app.query_one(Input) @@ -80,6 +86,7 @@ async def test_input_selected_text(): assert input_widget.selected_text == "" +@pytest.mark.anyio async def test_input_selection_deleted_programmatically(): async with InputApp().run_test() as pilot: input_widget = pilot.app.query_one(Input) diff --git a/tests/input/test_input_replace.py b/tests/input/test_input_replace.py index 40276ffab1..21e1513dc6 100644 --- a/tests/input/test_input_replace.py +++ b/tests/input/test_input_replace.py @@ -21,6 +21,7 @@ @pytest.mark.parametrize("selection,value_after", DELETIONS) +@pytest.mark.anyio async def test_input_delete(selection: tuple[int, int], value_after: str): class InputApp(App[None]): TEST_TEXT = "0123456789" @@ -50,6 +51,7 @@ def compose(self) -> ComposeResult: @pytest.mark.parametrize("selection,replacement,result", REPLACEMENTS) +@pytest.mark.anyio async def test_input_replace(selection: tuple[int, int], replacement: str, result: str): class InputApp(App[None]): TEST_TEXT = "0123456789" diff --git a/tests/input/test_input_restrict.py b/tests/input/test_input_restrict.py index d0465ec2e2..f1af52cd99 100644 --- a/tests/input/test_input_restrict.py +++ b/tests/input/test_input_restrict.py @@ -60,6 +60,7 @@ def test_input_integer_type(): assert not re.fullmatch(integer, "foo") +@pytest.mark.anyio async def test_bad_type(): """Check an invalid type raises a ValueError.""" @@ -74,6 +75,7 @@ def compose(self) -> ComposeResult: pass +@pytest.mark.anyio async def test_max_length(): """Check max_length limits characters.""" @@ -104,6 +106,7 @@ def compose(self) -> ComposeResult: assert input_widget.value == "12340" +@pytest.mark.anyio async def test_restrict(): """Test restriction by regex.""" @@ -129,6 +132,7 @@ def compose(self) -> ComposeResult: assert input_widget.value == "abca" +@pytest.mark.anyio async def test_restrict_type(): class InputApp(App): def compose(self) -> ComposeResult: diff --git a/tests/input/test_input_terminal_cursor.py b/tests/input/test_input_terminal_cursor.py index 31f1770181..80a80fd266 100644 --- a/tests/input/test_input_terminal_cursor.py +++ b/tests/input/test_input_terminal_cursor.py @@ -13,6 +13,7 @@ def compose(self) -> ComposeResult: yield Input("こんにちは!", select_on_focus=False) +@pytest.mark.anyio async def test_initial_terminal_cursor_position(): app = InputApp() async with app.run_test(): @@ -20,6 +21,7 @@ async def test_initial_terminal_cursor_position(): assert app.cursor_position == Offset(21, 5) +@pytest.mark.anyio async def test_terminal_cursor_position_update_on_cursor_move(): app = InputApp() async with app.run_test(): diff --git a/tests/input/test_input_validation.py b/tests/input/test_input_validation.py index 3ea17191b5..0bc6e69e8f 100644 --- a/tests/input/test_input_validation.py +++ b/tests/input/test_input_validation.py @@ -25,6 +25,7 @@ def on_changed_or_submitted(self, event): self.messages.append(event) +@pytest.mark.anyio async def test_input_changed_message_validation_failure(): app = InputApp() async with app.run_test() as pilot: @@ -44,6 +45,7 @@ async def test_input_changed_message_validation_failure(): ) +@pytest.mark.anyio async def test_input_changed_message_validation_success(): app = InputApp() async with app.run_test() as pilot: @@ -54,6 +56,7 @@ async def test_input_changed_message_validation_success(): assert app.messages[0].validation_result == ValidationResult.success() +@pytest.mark.anyio async def test_input_submitted_message_validation_failure(): app = InputApp() async with app.run_test() as pilot: @@ -73,6 +76,7 @@ async def test_input_submitted_message_validation_failure(): ) +@pytest.mark.anyio async def test_input_submitted_message_validation_success(): app = InputApp() async with app.run_test() as pilot: @@ -84,6 +88,7 @@ async def test_input_submitted_message_validation_success(): assert app.messages[1].validation_result == ValidationResult.success() +@pytest.mark.anyio async def test_on_blur_triggers_validation(): app = InputApp() async with app.run_test() as pilot: @@ -107,6 +112,7 @@ async def test_on_blur_triggers_validation(): ["fried", "garbage"], ], ) +@pytest.mark.anyio async def test_validation_on_changed_should_not_happen(validate_on): app = InputApp(validate_on) async with app.run_test() as pilot: @@ -134,6 +140,7 @@ async def test_validation_on_changed_should_not_happen(validate_on): {"fried", "garbage"}, ], ) +@pytest.mark.anyio async def test_validation_on_submitted_should_not_happen(validate_on): app = InputApp(validate_on) async with app.run_test() as pilot: @@ -161,6 +168,7 @@ async def test_validation_on_submitted_should_not_happen(validate_on): {"fried", "garbage"}, ], ) +@pytest.mark.anyio async def test_validation_on_blur_should_not_happen_unless_specified(validate_on): app = InputApp(validate_on) async with app.run_test() as pilot: @@ -177,6 +185,7 @@ async def test_validation_on_blur_should_not_happen_unless_specified(validate_on assert not input.has_class("-invalid") +@pytest.mark.anyio async def test_none_validate_on_means_all_validations_happen(): app = InputApp(None) async with app.run_test() as pilot: @@ -208,6 +217,7 @@ async def test_none_validate_on_means_all_validations_happen(): assert input.has_class("-valid") +@pytest.mark.anyio async def test_valid_empty(): app = InputApp(None) async with app.run_test() as pilot: diff --git a/tests/input/test_select_on_focus.py b/tests/input/test_select_on_focus.py index d804ac6ee6..7aca00c6bd 100644 --- a/tests/input/test_select_on_focus.py +++ b/tests/input/test_select_on_focus.py @@ -13,6 +13,7 @@ def compose(self) -> ComposeResult: yield Input("Hello, world!") +@pytest.mark.anyio async def test_focus_from_app_focus_does_not_select(): """When an Input has focused and the *app* is blurred and then focused (e.g. by pressing alt+tab or focusing another terminal pane), then the content of the Input should not be diff --git a/tests/layouts/test_horizontal.py b/tests/layouts/test_horizontal.py index 62d766aecb..0ea271bdf9 100644 --- a/tests/layouts/test_horizontal.py +++ b/tests/layouts/test_horizontal.py @@ -22,6 +22,7 @@ def compose(self) -> ComposeResult: yield app +@pytest.mark.anyio async def test_horizontal_get_content_width(app): async with app.run_test(): size = app.screen.size diff --git a/tests/listview/test_inherit_listview.py b/tests/listview/test_inherit_listview.py index 7a0e636af1..59b95f15bc 100644 --- a/tests/listview/test_inherit_listview.py +++ b/tests/listview/test_inherit_listview.py @@ -34,6 +34,7 @@ def compose(self) -> ComposeResult: yield MyListView(self._items) +@pytest.mark.anyio async def test_empty_inherited_list_view() -> None: """An empty self-populating inherited ListView should work as expected.""" async with ListViewApp().run_test() as pilot: @@ -43,6 +44,7 @@ async def test_empty_inherited_list_view() -> None: assert pilot.app.query_one(MyListView).index is None +@pytest.mark.anyio async def test_populated_inherited_list_view() -> None: """A self-populating inherited ListView should work as normal.""" async with ListViewApp(30).run_test() as pilot: @@ -52,6 +54,7 @@ async def test_populated_inherited_list_view() -> None: assert pilot.app.query_one(MyListView).index == 1 +@pytest.mark.anyio async def test_actions_work_when_list_view_empty() -> None: """Regression test for https://github.com/Textualize/textual/issues/2265""" async with ListViewApp().run_test() as pilot: diff --git a/tests/listview/test_listview_initial_index.py b/tests/listview/test_listview_initial_index.py index ab2eed573e..a957f13f6f 100644 --- a/tests/listview/test_listview_initial_index.py +++ b/tests/listview/test_listview_initial_index.py @@ -18,6 +18,7 @@ (8, 1), ], ) +@pytest.mark.anyio async def test_listview_initial_index(initial_index, expected_index) -> None: """Regression test for https://github.com/Textualize/textual/issues/4449""" diff --git a/tests/listview/test_listview_navigation.py b/tests/listview/test_listview_navigation.py index 680b8ced68..8384651f19 100644 --- a/tests/listview/test_listview_navigation.py +++ b/tests/listview/test_listview_navigation.py @@ -24,6 +24,7 @@ def _on_list_view_highlighted(self, message: ListView.Highlighted) -> None: self.highlighted.append(str(message.item.children[0].renderable)) +@pytest.mark.anyio async def test_keyboard_navigation_with_disabled_items() -> None: """Regression test for https://github.com/Textualize/textual/issues/3881.""" diff --git a/tests/listview/test_listview_remove_items.py b/tests/listview/test_listview_remove_items.py index 48a03633dd..128fe61a7d 100644 --- a/tests/listview/test_listview_remove_items.py +++ b/tests/listview/test_listview_remove_items.py @@ -11,6 +11,7 @@ def compose(self) -> ComposeResult: yield ListView() +@pytest.mark.anyio async def test_listview_pop_empty_raises_index_error(): app = EmptyListViewApp() async with app.run_test() as pilot: @@ -47,6 +48,7 @@ def _on_list_view_highlighted(self, message: ListView.Highlighted) -> None: self.highlighted.append(str(message.item.children[0].renderable)) +@pytest.mark.anyio async def test_listview_remove_items() -> None: """Regression test for https://github.com/Textualize/textual/issues/4735""" app = ListViewApp() @@ -67,6 +69,7 @@ async def test_listview_remove_items() -> None: (4, -2, 4, ["4"]), # Remove item after the highlighted index ], ) +@pytest.mark.anyio async def test_listview_pop_updates_index_and_highlighting( initial_index, pop_index, expected_new_index, expected_highlighted ) -> None: @@ -94,6 +97,7 @@ async def test_listview_pop_updates_index_and_highlighting( (4, range(0, 9), None, ["4", None]), # Remove all items ], ) +@pytest.mark.anyio async def test_listview_remove_items_updates_index_and_highlighting( initial_index, remove_indices, expected_new_index, expected_highlighted ) -> None: diff --git a/tests/notifications/test_all_levels_notifications.py b/tests/notifications/test_all_levels_notifications.py index 2b9c4be32b..83ba33aa27 100644 --- a/tests/notifications/test_all_levels_notifications.py +++ b/tests/notifications/test_all_levels_notifications.py @@ -22,6 +22,7 @@ def on_mount(self) -> None: self.push_screen(NotifyScreen()) +@pytest.mark.anyio async def test_all_levels_of_notification() -> None: """All levels within the DOM should be able to notify.""" async with NotifyApp().run_test() as pilot: diff --git a/tests/notifications/test_app_notifications.py b/tests/notifications/test_app_notifications.py index d5230bd919..bb18db6527 100644 --- a/tests/notifications/test_app_notifications.py +++ b/tests/notifications/test_app_notifications.py @@ -7,12 +7,14 @@ class NotificationApp(App[None]): pass +@pytest.mark.anyio async def test_app_no_notifications() -> None: """An app with no notifications should have an empty notification list.""" async with NotificationApp().run_test() as pilot: assert len(pilot.app._notifications) == 0 +@pytest.mark.anyio async def test_app_with_notifications() -> None: """An app with notifications should have notifications in the list.""" async with NotificationApp().run_test() as pilot: @@ -21,6 +23,7 @@ async def test_app_with_notifications() -> None: assert len(pilot.app._notifications) == 1 +@pytest.mark.anyio async def test_app_with_removing_notifications() -> None: """An app with notifications should have notifications in the list, which can be removed.""" async with NotificationApp().run_test() as pilot: @@ -31,6 +34,7 @@ async def test_app_with_removing_notifications() -> None: assert len(pilot.app._notifications) == 0 +@pytest.mark.anyio async def test_app_with_notifications_that_expire() -> None: """Notifications should expire from an app.""" async with NotificationApp().run_test() as pilot: @@ -42,6 +46,7 @@ async def test_app_with_notifications_that_expire() -> None: assert len(pilot.app._notifications) == 5 +@pytest.mark.anyio async def test_app_clearing_notifications() -> None: """The application should be able to clear all notifications.""" async with NotificationApp().run_test() as pilot: diff --git a/tests/option_list/test_option_list_create.py b/tests/option_list/test_option_list_create.py index 03c0c8772f..a3cef615fa 100644 --- a/tests/option_list/test_option_list_create.py +++ b/tests/option_list/test_option_list_create.py @@ -24,6 +24,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_all_parameters_become_options() -> None: """All input parameters to a list should become options.""" async with OptionListApp().run_test() as pilot: @@ -33,6 +34,7 @@ async def test_all_parameters_become_options() -> None: assert isinstance(option_list.get_option_at_index(n), Option) +@pytest.mark.anyio async def test_id_capture() -> None: """All options given an ID should retain the ID.""" async with OptionListApp().run_test() as pilot: @@ -48,6 +50,7 @@ async def test_id_capture() -> None: assert without_id == 3 +@pytest.mark.anyio async def test_get_option_by_id() -> None: """It should be possible to get an option by ID.""" async with OptionListApp().run_test() as pilot: @@ -56,6 +59,7 @@ async def test_get_option_by_id() -> None: assert option_list.get_option("4").prompt == "4" +@pytest.mark.anyio async def test_get_option_with_bad_id() -> None: """Asking for an option with a bad ID should give an error.""" async with OptionListApp().run_test() as pilot: @@ -63,6 +67,7 @@ async def test_get_option_with_bad_id() -> None: _ = pilot.app.query_one(OptionList).get_option("this does not exist") +@pytest.mark.anyio async def test_get_option_by_index() -> None: """It should be possible to get an option by index.""" async with OptionListApp().run_test() as pilot: @@ -72,6 +77,7 @@ async def test_get_option_by_index() -> None: assert option_list.get_option_at_index(-1).prompt == "4" +@pytest.mark.anyio async def test_get_option_at_bad_index() -> None: """Asking for an option at a bad index should give an error.""" async with OptionListApp().run_test() as pilot: @@ -81,6 +87,7 @@ async def test_get_option_at_bad_index() -> None: _ = pilot.app.query_one(OptionList).get_option_at_index(-42) +@pytest.mark.anyio async def test_clear_option_list() -> None: """It should be possible to clear the option list of all content.""" async with OptionListApp().run_test() as pilot: @@ -90,6 +97,7 @@ async def test_clear_option_list() -> None: assert option_list.option_count == 0 +@pytest.mark.anyio async def test_add_later() -> None: """It should be possible to add more items to a list.""" async with OptionListApp().run_test() as pilot: @@ -111,6 +119,7 @@ async def test_add_later() -> None: assert option_list.option_count == 10 +@pytest.mark.anyio async def test_create_with_duplicate_id() -> None: """Adding an option with a duplicate ID should be an error.""" async with OptionListApp().run_test() as pilot: @@ -121,6 +130,7 @@ async def test_create_with_duplicate_id() -> None: assert option_list.option_count == 5 +@pytest.mark.anyio async def test_create_with_duplicate_id_and_subsequent_non_dupes() -> None: """Adding an option with a duplicate ID should be an error.""" async with OptionListApp().run_test() as pilot: @@ -135,6 +145,7 @@ async def test_create_with_duplicate_id_and_subsequent_non_dupes() -> None: assert option_list.option_count == 7 +@pytest.mark.anyio async def test_adding_multiple_duplicates_at_once() -> None: """Adding duplicates together than aren't existing duplicates should be an error.""" async with OptionListApp().run_test() as pilot: @@ -150,6 +161,7 @@ async def test_adding_multiple_duplicates_at_once() -> None: assert option_list.option_count == 5 +@pytest.mark.anyio async def test_options_are_available_soon() -> None: """Regression test for https://github.com/Textualize/textual/issues/3903.""" diff --git a/tests/option_list/test_option_list_disabled.py b/tests/option_list/test_option_list_disabled.py index ed209bf50d..da6f580585 100644 --- a/tests/option_list/test_option_list_disabled.py +++ b/tests/option_list/test_option_list_disabled.py @@ -26,6 +26,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_default_enabled() -> None: """Options created enabled should remain enabled.""" async with OptionListApp(False).run_test() as pilot: @@ -34,6 +35,7 @@ async def test_default_enabled() -> None: assert option_list.get_option_at_index(option).disabled is False +@pytest.mark.anyio async def test_default_disabled() -> None: """Options created disabled should remain disabled.""" async with OptionListApp(True).run_test() as pilot: @@ -42,6 +44,7 @@ async def test_default_disabled() -> None: assert option_list.get_option_at_index(option).disabled is True +@pytest.mark.anyio async def test_enabled_to_disabled_via_index() -> None: """It should be possible to change enabled to disabled via index.""" async with OptionListApp(False).run_test() as pilot: @@ -52,6 +55,7 @@ async def test_enabled_to_disabled_via_index() -> None: assert option_list.get_option_at_index(n).disabled is True +@pytest.mark.anyio async def test_disabled_to_enabled_via_index() -> None: """It should be possible to change disabled to enabled via index.""" async with OptionListApp(True).run_test() as pilot: @@ -62,6 +66,7 @@ async def test_disabled_to_enabled_via_index() -> None: assert option_list.get_option_at_index(n).disabled is False +@pytest.mark.anyio async def test_enabled_to_disabled_via_id() -> None: """It should be possible to change enabled to disabled via id.""" async with OptionListApp(False).run_test() as pilot: @@ -72,6 +77,7 @@ async def test_enabled_to_disabled_via_id() -> None: assert option_list.get_option(str(n)).disabled is True +@pytest.mark.anyio async def test_disabled_to_enabled_via_id() -> None: """It should be possible to change disabled to enabled via id.""" async with OptionListApp(True).run_test() as pilot: @@ -82,6 +88,7 @@ async def test_disabled_to_enabled_via_id() -> None: assert option_list.get_option(str(n)).disabled is False +@pytest.mark.anyio async def test_disable_invalid_id() -> None: """Disabling an option via an ID that does not exist should throw an error.""" async with OptionListApp(True).run_test() as pilot: @@ -89,6 +96,7 @@ async def test_disable_invalid_id() -> None: pilot.app.query_one(OptionList).disable_option("does-not-exist") +@pytest.mark.anyio async def test_disable_invalid_index() -> None: """Disabling an option via an index that does not exist should throw an error.""" async with OptionListApp(True).run_test() as pilot: @@ -96,6 +104,7 @@ async def test_disable_invalid_index() -> None: pilot.app.query_one(OptionList).disable_option_at_index(4242) +@pytest.mark.anyio async def test_enable_invalid_id() -> None: """Disabling an option via an ID that does not exist should throw an error.""" async with OptionListApp(False).run_test() as pilot: @@ -103,6 +112,7 @@ async def test_enable_invalid_id() -> None: pilot.app.query_one(OptionList).enable_option("does-not-exist") +@pytest.mark.anyio async def test_enable_invalid_index() -> None: """Disabling an option via an index that does not exist should throw an error.""" async with OptionListApp(False).run_test() as pilot: diff --git a/tests/option_list/test_option_list_id_stability.py b/tests/option_list/test_option_list_id_stability.py index bd746914b0..fe57e4c3df 100644 --- a/tests/option_list/test_option_list_id_stability.py +++ b/tests/option_list/test_option_list_id_stability.py @@ -14,6 +14,7 @@ def compose(self) -> ComposeResult: yield OptionList() +@pytest.mark.anyio async def test_get_after_add() -> None: """It should be possible to get an option by ID after adding.""" async with OptionListApp().run_test() as pilot: diff --git a/tests/option_list/test_option_list_mouse_click.py b/tests/option_list/test_option_list_mouse_click.py index e41d9c5289..b080de002c 100644 --- a/tests/option_list/test_option_list_mouse_click.py +++ b/tests/option_list/test_option_list_mouse_click.py @@ -24,6 +24,7 @@ def log_option_message( self.messages.append(event.__class__.__name__) +@pytest.mark.anyio async def test_option_list_clicking_separator() -> None: """Regression test for https://github.com/Textualize/textual/issues/4710""" app = OptionListApp() diff --git a/tests/option_list/test_option_list_mouse_hover.py b/tests/option_list/test_option_list_mouse_hover.py index 73c6ef84b5..3fda38e1ef 100644 --- a/tests/option_list/test_option_list_mouse_hover.py +++ b/tests/option_list/test_option_list_mouse_hover.py @@ -18,6 +18,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_no_hover() -> None: """When the mouse isn't over the OptionList _mouse_hovering_over should be None.""" async with OptionListApp().run_test() as pilot: @@ -25,6 +26,7 @@ async def test_no_hover() -> None: assert pilot.app.query_one(OptionList)._mouse_hovering_over is None +@pytest.mark.anyio async def test_hover_highlight() -> None: """The mouse hover value should react to the mouse hover over a highlighted option.""" async with OptionListApp().run_test() as pilot: @@ -34,6 +36,7 @@ async def test_hover_highlight() -> None: assert option_list._mouse_hovering_over == option_list.highlighted +@pytest.mark.anyio async def test_hover_no_highlight() -> None: """The mouse hover value should react to the mouse hover over a non-highlighted option.""" async with OptionListApp().run_test() as pilot: @@ -43,6 +46,7 @@ async def test_hover_no_highlight() -> None: assert option_list._mouse_hovering_over != option_list.highlighted +@pytest.mark.anyio async def test_hover_disabled() -> None: """The mouse hover value should react to the mouse hover over a disabled option.""" async with OptionListApp().run_test() as pilot: @@ -55,6 +59,7 @@ async def test_hover_disabled() -> None: assert option_list._mouse_hovering_over != option_list.highlighted +@pytest.mark.anyio async def test_hover_then_leave() -> None: """After a mouse has been over an OptionList and left _mouse_hovering_over should be None again.""" async with OptionListApp().run_test() as pilot: diff --git a/tests/option_list/test_option_list_movement.py b/tests/option_list/test_option_list_movement.py index 447d0ba3c8..17b09fe4a6 100644 --- a/tests/option_list/test_option_list_movement.py +++ b/tests/option_list/test_option_list_movement.py @@ -16,12 +16,14 @@ def compose(self) -> ComposeResult: yield OptionList("1", "2", "3", None, "4", "5", "6") +@pytest.mark.anyio async def test_initial_highlight() -> None: """The highlight should start on the first item.""" async with OptionListApp().run_test() as pilot: assert pilot.app.query_one(OptionList).highlighted == 0 +@pytest.mark.anyio async def test_cleared_highlight_is_none() -> None: """The highlight should be `None` if the list is cleared.""" async with OptionListApp().run_test() as pilot: @@ -30,6 +32,7 @@ async def test_cleared_highlight_is_none() -> None: assert option_list.highlighted is None +@pytest.mark.anyio async def test_cleared_movement_does_nothing() -> None: """The highlight should remain `None` if the list is cleared.""" async with OptionListApp().run_test() as pilot: @@ -40,6 +43,7 @@ async def test_cleared_movement_does_nothing() -> None: assert option_list.highlighted is None +@pytest.mark.anyio async def test_move_down() -> None: """The highlight should move down when asked to.""" async with OptionListApp().run_test() as pilot: @@ -47,6 +51,7 @@ async def test_move_down() -> None: assert pilot.app.query_one(OptionList).highlighted == 1 +@pytest.mark.anyio async def test_move_down_from_end() -> None: """The highlight should wrap around when moving down from the end.""" async with OptionListApp().run_test() as pilot: @@ -56,6 +61,7 @@ async def test_move_down_from_end() -> None: assert option_list.highlighted == 0 +@pytest.mark.anyio async def test_move_up() -> None: """The highlight should move up when asked to.""" async with OptionListApp().run_test() as pilot: @@ -65,6 +71,7 @@ async def test_move_up() -> None: assert option_list.highlighted == 0 +@pytest.mark.anyio async def test_move_up_from_nowhere() -> None: """The highlight should settle on the last item when moving up from `None`.""" async with OptionListApp().run_test() as pilot: @@ -72,6 +79,7 @@ async def test_move_up_from_nowhere() -> None: assert pilot.app.query_one(OptionList).highlighted == 5 +@pytest.mark.anyio async def test_move_end() -> None: """The end key should go to the end of the list.""" async with OptionListApp().run_test() as pilot: @@ -79,6 +87,7 @@ async def test_move_end() -> None: assert pilot.app.query_one(OptionList).highlighted == 5 +@pytest.mark.anyio async def test_move_home() -> None: """The home key should go to the start of the list.""" async with OptionListApp().run_test() as pilot: @@ -90,6 +99,7 @@ async def test_move_home() -> None: assert option_list.highlighted == 0 +@pytest.mark.anyio async def test_page_down_from_start_short_list() -> None: """Doing a page down from the start of a short list should move to the end.""" async with OptionListApp().run_test() as pilot: @@ -97,6 +107,7 @@ async def test_page_down_from_start_short_list() -> None: assert pilot.app.query_one(OptionList).highlighted == 5 +@pytest.mark.anyio async def test_page_up_from_end_short_list() -> None: """Doing a page up from the end of a short list should move to the start.""" async with OptionListApp().run_test() as pilot: @@ -108,6 +119,7 @@ async def test_page_up_from_end_short_list() -> None: assert option_list.highlighted == 0 +@pytest.mark.anyio async def test_page_down_from_end_short_list() -> None: """Doing a page down from the end of a short list should go nowhere.""" async with OptionListApp().run_test() as pilot: @@ -119,6 +131,7 @@ async def test_page_down_from_end_short_list() -> None: assert option_list.highlighted == 5 +@pytest.mark.anyio async def test_page_up_from_start_short_list() -> None: """Doing a page up from the start of a short list go nowhere.""" async with OptionListApp().run_test() as pilot: @@ -133,6 +146,7 @@ def compose(self) -> ComposeResult: yield OptionList() +@pytest.mark.anyio async def test_empty_list_movement() -> None: """Attempting to move around an empty list should be a non-operation.""" async with EmptyOptionListApp().run_test() as pilot: @@ -154,6 +168,7 @@ async def test_empty_list_movement() -> None: ("pagedown", 99), ], ) +@pytest.mark.anyio async def test_no_highlight_movement(movement: str, landing: int) -> None: """Attempting to move around in a list with no highlight should select the most appropriate item.""" async with EmptyOptionListApp().run_test() as pilot: @@ -186,6 +201,7 @@ def _on_option_list_option_highlighted( self.highlighted.append(str(message.option.prompt)) +@pytest.mark.anyio async def test_keyboard_navigation_with_disabled_options() -> None: """Regression test for https://github.com/Textualize/textual/issues/3881.""" diff --git a/tests/option_list/test_option_list_option_subclass.py b/tests/option_list/test_option_list_option_subclass.py index 9e59c55ddb..bf57a170ee 100644 --- a/tests/option_list/test_option_list_option_subclass.py +++ b/tests/option_list/test_option_list_option_subclass.py @@ -22,6 +22,7 @@ def compose(self) -> ComposeResult: yield OptionList(*[OptionWithExtras(n) for n in range(100)]) +@pytest.mark.anyio async def test_option_list_with_subclassed_options() -> None: """It should be possible to build an option list with subclassed options.""" async with OptionListApp().run_test() as pilot: diff --git a/tests/option_list/test_option_messages.py b/tests/option_list/test_option_messages.py index 7d0d899653..e4bf45ace1 100644 --- a/tests/option_list/test_option_messages.py +++ b/tests/option_list/test_option_messages.py @@ -34,6 +34,7 @@ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> No self._record(event) +@pytest.mark.anyio async def test_messages_on_startup() -> None: """There should be a highlighted message when a non-empty option list first starts up.""" async with OptionListApp().run_test() as pilot: @@ -42,6 +43,7 @@ async def test_messages_on_startup() -> None: assert pilot.app.messages == [("OptionHighlighted", "0", 0)] +@pytest.mark.anyio async def test_same_highlight_message() -> None: """Highlighting a highlight should result in no message.""" async with OptionListApp().run_test() as pilot: @@ -52,6 +54,7 @@ async def test_same_highlight_message() -> None: assert pilot.app.messages == [("OptionHighlighted", "0", 0)] +@pytest.mark.anyio async def test_highlight_disabled_option_no_message() -> None: """Highlighting a disabled option should result in no messages.""" async with OptionListApp().run_test() as pilot: @@ -63,6 +66,7 @@ async def test_highlight_disabled_option_no_message() -> None: assert pilot.app.messages[1:] == [] +@pytest.mark.anyio async def test_new_highlight() -> None: """Setting the highlight to a new option should result in a message.""" async with OptionListApp().run_test() as pilot: @@ -73,6 +77,7 @@ async def test_new_highlight() -> None: assert pilot.app.messages[1:] == [("OptionHighlighted", "2", 2)] +@pytest.mark.anyio async def test_move_highlight_with_keyboard() -> None: """Changing option via the keyboard should result in a message.""" async with OptionListApp().run_test() as pilot: @@ -81,6 +86,7 @@ async def test_move_highlight_with_keyboard() -> None: assert pilot.app.messages[1:] == [("OptionHighlighted", "1", 1)] +@pytest.mark.anyio async def test_select_message_with_keyboard() -> None: """Hitting enter on an option should result in a message.""" async with OptionListApp().run_test() as pilot: @@ -92,6 +98,7 @@ async def test_select_message_with_keyboard() -> None: ] +@pytest.mark.anyio async def test_click_option_with_mouse() -> None: """Clicking on an option via the mouse should result in highlight and select messages.""" async with OptionListApp().run_test() as pilot: @@ -103,6 +110,7 @@ async def test_click_option_with_mouse() -> None: ] +@pytest.mark.anyio async def test_click_disabled_option_with_mouse() -> None: """Clicking on a disabled option via the mouse should result no messages.""" async with OptionListApp().run_test() as pilot: diff --git a/tests/option_list/test_option_prompt_replacement.py b/tests/option_list/test_option_prompt_replacement.py index 4bcb2f7ca5..b87741fb29 100644 --- a/tests/option_list/test_option_prompt_replacement.py +++ b/tests/option_list/test_option_prompt_replacement.py @@ -17,6 +17,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_replace_option_prompt_with_invalid_id() -> None: """Attempting to replace the prompt of an option ID that doesn't exist should raise an exception.""" async with OptionListApp().run_test() as pilot: @@ -26,6 +27,7 @@ async def test_replace_option_prompt_with_invalid_id() -> None: ) +@pytest.mark.anyio async def test_replace_option_prompt_with_invalid_index() -> None: """Attempting to replace the prompt of an option index that doesn't exist should raise an exception.""" async with OptionListApp().run_test() as pilot: @@ -35,6 +37,7 @@ async def test_replace_option_prompt_with_invalid_index() -> None: ) +@pytest.mark.anyio async def test_replace_option_prompt_with_valid_id() -> None: """It should be possible to replace the prompt of an option ID that does exist.""" async with OptionListApp().run_test() as pilot: @@ -43,6 +46,7 @@ async def test_replace_option_prompt_with_valid_id() -> None: assert option_list.get_option("0").prompt == "new-prompt" +@pytest.mark.anyio async def test_replace_option_prompt_with_valid_index() -> None: """It should be possible to replace the prompt of an option index that does exist.""" async with OptionListApp().run_test() as pilot: @@ -52,6 +56,7 @@ async def test_replace_option_prompt_with_valid_index() -> None: assert option_list.get_option_at_index(1).prompt == "new-prompt" +@pytest.mark.anyio async def test_replace_single_line_option_prompt_with_multiple() -> None: """It should be possible to replace single line prompt with multiple lines""" new_prompt = "new-prompt\nsecond line" @@ -61,6 +66,7 @@ async def test_replace_single_line_option_prompt_with_multiple() -> None: assert option_list.get_option("0").prompt == new_prompt +@pytest.mark.anyio async def test_replace_multiple_line_option_prompt_with_single() -> None: """It should be possible to replace multiple line prompt with a single line""" new_prompt = "new-prompt" @@ -70,6 +76,7 @@ async def test_replace_multiple_line_option_prompt_with_single() -> None: assert option_list.get_option("0").prompt == new_prompt +@pytest.mark.anyio async def test_replace_multiple_line_option_prompt_with_multiple() -> None: """It should be possible to replace multiple line prompt with multiple lines""" new_prompt = "new-prompt\nsecond line" diff --git a/tests/option_list/test_option_removal.py b/tests/option_list/test_option_removal.py index a26f5af496..79790dbb37 100644 --- a/tests/option_list/test_option_removal.py +++ b/tests/option_list/test_option_removal.py @@ -20,6 +20,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_remove_first_option_via_index() -> None: """It should be possible to remove the first option of an option list, via index.""" async with OptionListApp().run_test() as pilot: @@ -31,6 +32,7 @@ async def test_remove_first_option_via_index() -> None: assert option_list.highlighted == 0 +@pytest.mark.anyio async def test_remove_first_option_via_id() -> None: """It should be possible to remove the first option of an option list, via ID.""" async with OptionListApp().run_test() as pilot: @@ -42,6 +44,7 @@ async def test_remove_first_option_via_id() -> None: assert option_list.highlighted == 0 +@pytest.mark.anyio async def test_remove_last_option_via_index() -> None: """It should be possible to remove the last option of an option list, via index.""" async with OptionListApp().run_test() as pilot: @@ -53,6 +56,7 @@ async def test_remove_last_option_via_index() -> None: assert option_list.highlighted == 0 +@pytest.mark.anyio async def test_remove_last_option_via_id() -> None: """It should be possible to remove the last option of an option list, via ID.""" async with OptionListApp().run_test() as pilot: @@ -64,6 +68,7 @@ async def test_remove_last_option_via_id() -> None: assert option_list.highlighted == 0 +@pytest.mark.anyio async def test_remove_all_options_via_index() -> None: """It should be possible to remove all options via index.""" async with OptionListApp().run_test() as pilot: @@ -76,6 +81,7 @@ async def test_remove_all_options_via_index() -> None: assert option_list.highlighted is None +@pytest.mark.anyio async def test_remove_all_options_via_id() -> None: """It should be possible to remove all options via ID.""" async with OptionListApp().run_test() as pilot: @@ -88,6 +94,7 @@ async def test_remove_all_options_via_id() -> None: assert option_list.highlighted is None +@pytest.mark.anyio async def test_remove_invalid_id() -> None: """Attempting to remove an option ID that doesn't exist should raise an exception.""" async with OptionListApp().run_test() as pilot: @@ -95,6 +102,7 @@ async def test_remove_invalid_id() -> None: pilot.app.query_one(OptionList).remove_option("does-not-exist") +@pytest.mark.anyio async def test_remove_invalid_index() -> None: """Attempting to remove an option index that doesn't exist should raise an exception.""" async with OptionListApp().run_test() as pilot: @@ -102,6 +110,7 @@ async def test_remove_invalid_index() -> None: pilot.app.query_one(OptionList).remove_option_at_index(23) +@pytest.mark.anyio async def test_remove_with_hover_on_last_option(): """https://github.com/Textualize/textual/issues/3270""" async with OptionListApp().run_test() as pilot: diff --git a/tests/select/test_blank_and_clear.py b/tests/select/test_blank_and_clear.py index 3d4f7dca66..a10e0c7cbe 100644 --- a/tests/select/test_blank_and_clear.py +++ b/tests/select/test_blank_and_clear.py @@ -7,6 +7,7 @@ SELECT_OPTIONS = [(str(n), n) for n in range(3)] +@pytest.mark.anyio async def test_value_is_blank_by_default(): class SelectApp(App[None]): def compose(self): @@ -19,6 +20,7 @@ def compose(self): assert select.is_blank() +@pytest.mark.anyio async def test_setting_and_checking_blank(): class SelectApp(App[None]): def compose(self): @@ -39,6 +41,7 @@ def compose(self): assert select.is_blank() +@pytest.mark.anyio async def test_clear_with_allow_blanks(): class SelectApp(App[None]): def compose(self): @@ -52,6 +55,7 @@ def compose(self): assert select.is_blank() +@pytest.mark.anyio async def test_clear_fails_if_allow_blank_is_false(): class SelectApp(App[None]): def compose(self): @@ -65,6 +69,7 @@ def compose(self): select.clear() +@pytest.mark.anyio async def test_selection_is_none_with_blank(): class SelectApp(App[None]): def compose(self): diff --git a/tests/select/test_changed_message.py b/tests/select/test_changed_message.py index 166d676972..93f5dcf49c 100644 --- a/tests/select/test_changed_message.py +++ b/tests/select/test_changed_message.py @@ -17,6 +17,7 @@ def add_message(self, event): self.changed_messages.append(event) +@pytest.mark.anyio async def test_message_control(): app = SelectApp() async with app.run_test() as pilot: @@ -27,6 +28,7 @@ async def test_message_control(): assert message.control is app.query_one(Select) +@pytest.mark.anyio async def test_selecting_posts_message(): app = SelectApp() async with app.run_test() as pilot: @@ -42,6 +44,7 @@ async def test_selecting_posts_message(): assert len(app.changed_messages) == 2 +@pytest.mark.anyio async def test_same_selection_does_not_post_message(): app = SelectApp() async with app.run_test() as pilot: @@ -57,6 +60,7 @@ async def test_same_selection_does_not_post_message(): assert len(app.changed_messages) == 1 +@pytest.mark.anyio async def test_setting_value_posts_message() -> None: """Setting the value of a Select should post a message.""" diff --git a/tests/select/test_empty_select.py b/tests/select/test_empty_select.py index 75bf722eb1..5a3a33984b 100644 --- a/tests/select/test_empty_select.py +++ b/tests/select/test_empty_select.py @@ -5,6 +5,7 @@ from textual.widgets.select import EmptySelectError +@pytest.mark.anyio async def test_empty_select_is_ok_with_blanks(): class SelectApp(App[None]): def compose(self): @@ -16,6 +17,7 @@ def compose(self): assert app.query_one(Select).is_blank() +@pytest.mark.anyio async def test_empty_set_options_is_ok_with_blanks(): class SelectApp(App[None]): def compose(self): @@ -29,6 +31,7 @@ def compose(self): assert select.is_blank() # Sanity check. +@pytest.mark.anyio async def test_empty_select_raises_exception_if_allow_blank_is_false(): class SelectApp(App[None]): def compose(self): @@ -40,6 +43,7 @@ def compose(self): pass +@pytest.mark.anyio async def test_empty_set_options_raises_exception_if_allow_blank_is_false(): class SelectApp(App[None]): def compose(self): diff --git a/tests/select/test_prompt.py b/tests/select/test_prompt.py index 2b441d73ab..3999739ff0 100644 --- a/tests/select/test_prompt.py +++ b/tests/select/test_prompt.py @@ -5,6 +5,7 @@ from textual.widgets._select import SelectCurrent, SelectOverlay +@pytest.mark.anyio async def test_reactive_prompt_change(): """Regression test for https://github.com/Textualize/textual/issues/2983""" @@ -30,6 +31,7 @@ def compose(self): assert select_overlay._options[0].prompt == Text("New prompt") +@pytest.mark.anyio async def test_reactive_prompt_change_when_allow_blank_is_false(): class SelectApp(App): def compose(self): diff --git a/tests/select/test_remove.py b/tests/select/test_remove.py index 989884a8c9..741a71590a 100644 --- a/tests/select/test_remove.py +++ b/tests/select/test_remove.py @@ -8,6 +8,7 @@ I will permit it to pass over me and through me.""".splitlines() +@pytest.mark.anyio async def test_select_remove(): # Regression test for https://github.com/Textualize/textual/issues/4782 class SelectApp(App): diff --git a/tests/select/test_value.py b/tests/select/test_value.py index c47e285005..252c417e32 100644 --- a/tests/select/test_value.py +++ b/tests/select/test_value.py @@ -17,6 +17,7 @@ def compose(self): yield Select[int](SELECT_OPTIONS, value=self.initial_value) +@pytest.mark.anyio async def test_initial_value_is_validated(): """The initial value should be respected if it is a legal value. @@ -27,6 +28,7 @@ async def test_initial_value_is_validated(): assert app.query_one(Select).value == 1 +@pytest.mark.anyio async def test_value_unknown_option_raises_error(): """Setting the value to an unknown value raises an error.""" app = SelectApp() @@ -35,6 +37,7 @@ async def test_value_unknown_option_raises_error(): app.query_one(Select).value = "french fries" +@pytest.mark.anyio async def test_initial_value_inside_compose_is_validated(): """Setting the value to an unknown value inside compose should raise an error.""" @@ -50,6 +53,7 @@ def compose(self): pass +@pytest.mark.anyio async def test_value_assign_to_blank(): """Setting the value to BLANK should work with default `allow_blank` value.""" app = SelectApp(1) @@ -60,6 +64,7 @@ async def test_value_assign_to_blank(): assert select.is_blank() +@pytest.mark.anyio async def test_initial_value_is_picked_if_allow_blank_is_false(): """The initial value should be picked by default if allow_blank=False.""" @@ -72,6 +77,7 @@ def compose(self): assert app.query_one(Select).value == 0 +@pytest.mark.anyio async def test_initial_value_is_picked_if_allow_blank_is_false(): """The initial value should be respected even if allow_blank=False.""" @@ -84,6 +90,7 @@ def compose(self): assert app.query_one(Select).value == 2 +@pytest.mark.anyio async def test_set_value_to_blank_with_allow_blank_false(): """Setting the value to BLANK with allow_blank=False should raise an error.""" @@ -97,6 +104,7 @@ def compose(self): app.query_one(Select).value = Select.BLANK +@pytest.mark.anyio async def test_set_options_resets_value_to_blank(): """Resetting the options should reset the value to BLANK.""" @@ -112,6 +120,7 @@ def compose(self): assert select.is_blank() +@pytest.mark.anyio async def test_set_options_resets_value_if_allow_blank_is_false(): """Resetting the options should reset the value if allow_blank=False.""" diff --git a/tests/selection_list/test_over_wide_selections.py b/tests/selection_list/test_over_wide_selections.py index 1103ff15da..90d8af6e3a 100644 --- a/tests/selection_list/test_over_wide_selections.py +++ b/tests/selection_list/test_over_wide_selections.py @@ -17,6 +17,7 @@ def compose(self) -> ComposeResult: yield SelectionList[int](*[(f"{n} ", n) for n in range(10)]) +@pytest.mark.anyio async def test_over_wide_options() -> None: """Options wider than the widget should not be an issue.""" async with SelectionListApp().run_test() as pilot: diff --git a/tests/selection_list/test_selection_click_checkbox.py b/tests/selection_list/test_selection_click_checkbox.py index 8ca8b0fe0f..e967bcf52a 100644 --- a/tests/selection_list/test_selection_click_checkbox.py +++ b/tests/selection_list/test_selection_click_checkbox.py @@ -22,6 +22,7 @@ def _record(self, event: SelectionList.SelectionToggled) -> None: self.clicks.append(event.selection_index) +@pytest.mark.anyio async def test_click_on_prompt() -> None: """It should be possible to toggle a selection by clicking on the prompt.""" async with SelectionListApp().run_test() as pilot: @@ -31,6 +32,7 @@ async def test_click_on_prompt() -> None: assert pilot.app.clicks == [0] +@pytest.mark.anyio async def test_click_on_checkbox() -> None: """It should be possible to toggle a selection by clicking on the checkbox.""" async with SelectionListApp().run_test() as pilot: diff --git a/tests/selection_list/test_selection_list_create.py b/tests/selection_list/test_selection_list_create.py index cd92b925d2..c733b43cb0 100644 --- a/tests/selection_list/test_selection_list_create.py +++ b/tests/selection_list/test_selection_list_create.py @@ -29,6 +29,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_all_parameters_become_selctions() -> None: """All input parameters to a list should become selections.""" async with SelectionListApp().run_test() as pilot: @@ -38,6 +39,7 @@ async def test_all_parameters_become_selctions() -> None: assert isinstance(selections.get_option_at_index(n), Selection) +@pytest.mark.anyio async def test_get_selection_by_index() -> None: """It should be possible to get a selection by index.""" async with SelectionListApp().run_test() as pilot: @@ -47,6 +49,7 @@ async def test_get_selection_by_index() -> None: assert str(option_list.get_option_at_index(-1).prompt) == "4" +@pytest.mark.anyio async def test_get_selection_by_id() -> None: """It should be possible to get a selection by ID.""" async with SelectionListApp().run_test() as pilot: @@ -55,6 +58,7 @@ async def test_get_selection_by_id() -> None: assert str(option_list.get_option("4").prompt) == "4" +@pytest.mark.anyio async def test_add_later() -> None: """It should be possible to add more items to a selection list.""" async with SelectionListApp().run_test() as pilot: @@ -72,6 +76,7 @@ async def test_add_later() -> None: assert selections.option_count == 11 +@pytest.mark.anyio async def test_add_later_selcted_state() -> None: """When adding selections later the selected collection should get updated.""" async with SelectionListApp().run_test() as pilot: @@ -83,6 +88,7 @@ async def test_add_later_selcted_state() -> None: assert selections.selected == [2, 4, 5, 6] +@pytest.mark.anyio async def test_add_non_selections() -> None: """Adding options that aren't selections should result in errors.""" async with SelectionListApp().run_test() as pilot: @@ -99,6 +105,7 @@ async def test_add_non_selections() -> None: selections.add_option(("Nope", 0, False, 23)) +@pytest.mark.anyio async def test_clear_options() -> None: """Clearing the options should also clear the selections.""" async with SelectionListApp().run_test() as pilot: @@ -107,6 +114,7 @@ async def test_clear_options() -> None: assert selections.selected == [] +@pytest.mark.anyio async def test_options_are_available_soon() -> None: """Regression test for https://github.com/Textualize/textual/issues/3903.""" @@ -115,6 +123,7 @@ async def test_options_are_available_soon() -> None: assert selection_list.get_option("some_id") is selection +@pytest.mark.anyio async def test_removing_option_updates_indexes() -> None: async with SelectionListApp().run_test() as pilot: selections = pilot.app.query_one(SelectionList) diff --git a/tests/selection_list/test_selection_messages.py b/tests/selection_list/test_selection_messages.py index 4e52ac4ccf..01edc86b09 100644 --- a/tests/selection_list/test_selection_messages.py +++ b/tests/selection_list/test_selection_messages.py @@ -47,6 +47,7 @@ def _record( ) +@pytest.mark.anyio async def test_messages_on_startup() -> None: """There should be a highlighted message when a non-empty selection list first starts up.""" async with SelectionListApp().run_test() as pilot: @@ -55,6 +56,7 @@ async def test_messages_on_startup() -> None: assert pilot.app.messages == [("SelectionHighlighted", 0)] +@pytest.mark.anyio async def test_new_highlight() -> None: """Setting the highlight to a new option should result in a message.""" async with SelectionListApp().run_test() as pilot: @@ -65,6 +67,7 @@ async def test_new_highlight() -> None: assert pilot.app.messages[1:] == [("SelectionHighlighted", 2)] +@pytest.mark.anyio async def test_toggle() -> None: """Toggling an option should result in messages.""" async with SelectionListApp().run_test() as pilot: @@ -79,6 +82,7 @@ async def test_toggle() -> None: ] +@pytest.mark.anyio async def test_toggle_via_user() -> None: """Toggling via the user should result in the correct messages.""" async with SelectionListApp().run_test() as pilot: @@ -92,6 +96,7 @@ async def test_toggle_via_user() -> None: ] +@pytest.mark.anyio async def test_toggle_all() -> None: """Toggling all options should result in messages.""" async with SelectionListApp().run_test() as pilot: @@ -115,6 +120,7 @@ async def test_toggle_all() -> None: ] +@pytest.mark.anyio async def test_select() -> None: """Selecting all an option should result in a message.""" async with SelectionListApp().run_test() as pilot: @@ -128,6 +134,7 @@ async def test_select() -> None: ] +@pytest.mark.anyio async def test_select_selected() -> None: """Selecting an option that is already selected should emit no extra message..""" async with SelectionListApp().run_test() as pilot: @@ -143,6 +150,7 @@ async def test_select_selected() -> None: ] +@pytest.mark.anyio async def test_select_all() -> None: """Selecting all options should result in messages.""" async with SelectionListApp().run_test() as pilot: @@ -156,6 +164,7 @@ async def test_select_all() -> None: ] +@pytest.mark.anyio async def test_select_all_selected() -> None: """Selecting all when all are selected should result in no extra messages.""" async with SelectionListApp().run_test() as pilot: @@ -171,6 +180,7 @@ async def test_select_all_selected() -> None: ] +@pytest.mark.anyio async def test_deselect() -> None: """Deselecting an option should result in a message.""" async with SelectionListApp().run_test() as pilot: @@ -187,6 +197,7 @@ async def test_deselect() -> None: ] +@pytest.mark.anyio async def test_deselect_deselected() -> None: """Deselecting a deselected option should result in no extra messages.""" async with SelectionListApp().run_test() as pilot: @@ -197,6 +208,7 @@ async def test_deselect_deselected() -> None: assert pilot.app.messages == [("SelectionHighlighted", 0)] +@pytest.mark.anyio async def test_deselect_all() -> None: """Deselecting all deselected options should result in no additional messages.""" async with SelectionListApp().run_test() as pilot: @@ -207,6 +219,7 @@ async def test_deselect_all() -> None: assert pilot.app.messages == [("SelectionHighlighted", 0)] +@pytest.mark.anyio async def test_select_then_deselect_all() -> None: """Selecting and then deselecting all options should result in messages.""" async with SelectionListApp().run_test() as pilot: diff --git a/tests/selection_list/test_selection_values.py b/tests/selection_list/test_selection_values.py index 0e2779b0ca..ab651ba8f5 100644 --- a/tests/selection_list/test_selection_values.py +++ b/tests/selection_list/test_selection_values.py @@ -15,12 +15,14 @@ def compose(self) -> ComposeResult: yield SelectionList[int](*[(str(n), n, self._default_state) for n in range(50)]) +@pytest.mark.anyio async def test_empty_selected() -> None: """Selected should be empty when nothing is selected.""" async with SelectionListApp().run_test() as pilot: assert pilot.app.query_one(SelectionList).selected == [] +@pytest.mark.anyio async def test_programatic_select() -> None: """Selected should contain a selected value.""" async with SelectionListApp().run_test() as pilot: @@ -29,6 +31,7 @@ async def test_programatic_select() -> None: assert pilot.app.query_one(SelectionList).selected == [0] +@pytest.mark.anyio async def test_programatic_select_all() -> None: """Selected should contain all selected values.""" async with SelectionListApp().run_test() as pilot: @@ -37,6 +40,7 @@ async def test_programatic_select_all() -> None: assert pilot.app.query_one(SelectionList).selected == list(range(50)) +@pytest.mark.anyio async def test_programatic_deselect() -> None: """Selected should not contain a deselected value.""" async with SelectionListApp(True).run_test() as pilot: @@ -45,6 +49,7 @@ async def test_programatic_deselect() -> None: assert pilot.app.query_one(SelectionList).selected == list(range(50)[1:]) +@pytest.mark.anyio async def test_programatic_deselect_all() -> None: """Selected should not contain anything after deselecting all values.""" async with SelectionListApp(True).run_test() as pilot: @@ -53,6 +58,7 @@ async def test_programatic_deselect_all() -> None: assert pilot.app.query_one(SelectionList).selected == [] +@pytest.mark.anyio async def test_programatic_toggle() -> None: """Selected should reflect a toggle.""" async with SelectionListApp().run_test() as pilot: @@ -64,6 +70,7 @@ async def test_programatic_toggle() -> None: assert pilot.app.query_one(SelectionList).selected == list(range(50)[:25]) +@pytest.mark.anyio async def test_programatic_toggle_all() -> None: """Selected should contain all values after toggling all on.""" async with SelectionListApp().run_test() as pilot: @@ -72,6 +79,7 @@ async def test_programatic_toggle_all() -> None: assert pilot.app.query_one(SelectionList).selected == list(range(50)) +@pytest.mark.anyio async def test_removal_of_selected_item() -> None: """Removing a selected selection should remove its value from the selected set.""" async with SelectionListApp().run_test() as pilot: diff --git a/tests/suggester/test_input_suggestions.py b/tests/suggester/test_input_suggestions.py index e70e415932..254d5f0f34 100644 --- a/tests/suggester/test_input_suggestions.py +++ b/tests/suggester/test_input_suggestions.py @@ -17,6 +17,7 @@ def compose(self) -> ComposeResult: yield self.input +@pytest.mark.anyio async def test_no_suggestions(): app = SuggestionsApp([]) async with app.run_test() as pilot: @@ -25,6 +26,7 @@ async def test_no_suggestions(): assert app.input._suggestion == "" +@pytest.mark.anyio async def test_suggestion(): app = SuggestionsApp(["hello"]) async with app.run_test() as pilot: @@ -33,6 +35,7 @@ async def test_suggestion(): assert app.input._suggestion == "hello" +@pytest.mark.anyio async def test_accept_suggestion(): app = SuggestionsApp(["hello"]) async with app.run_test() as pilot: @@ -41,12 +44,14 @@ async def test_accept_suggestion(): assert app.input.value == "hello" +@pytest.mark.anyio async def test_no_suggestion_on_empty_value(): app = SuggestionsApp(["hello"]) async with app.run_test(): assert app.input._suggestion == "" +@pytest.mark.anyio async def test_no_suggestion_on_empty_value_after_deleting(): app = SuggestionsApp(["hello"]) async with app.run_test() as pilot: @@ -55,6 +60,7 @@ async def test_no_suggestion_on_empty_value_after_deleting(): assert app.input._suggestion == "" +@pytest.mark.anyio async def test_suggestion_shows_up_after_deleting_extra_chars(): app = SuggestionsApp(["hello"]) async with app.run_test() as pilot: @@ -64,6 +70,7 @@ async def test_suggestion_shows_up_after_deleting_extra_chars(): assert app.input._suggestion == "hello" +@pytest.mark.anyio async def test_suggestion_shows_up_after_deleting_extra_chars_in_middle_of_word(): app = SuggestionsApp(["hello"]) async with app.run_test() as pilot: @@ -85,6 +92,7 @@ async def test_suggestion_shows_up_after_deleting_extra_chars_in_middle_of_word( (string.punctuation[::3], 5), ], ) +@pytest.mark.anyio async def test_suggestion_with_special_characters(suggestion: str, truncate_at: int): app = SuggestionsApp([suggestion]) async with app.run_test() as pilot: @@ -92,6 +100,7 @@ async def test_suggestion_with_special_characters(suggestion: str, truncate_at: assert app.input._suggestion == suggestion +@pytest.mark.anyio async def test_suggestion_priority(): app = SuggestionsApp(["dog", "dad"]) async with app.run_test() as pilot: diff --git a/tests/suggester/test_suggest_from_list.py b/tests/suggester/test_suggest_from_list.py index 833c5e4b20..c006ce4641 100644 --- a/tests/suggester/test_suggest_from_list.py +++ b/tests/suggester/test_suggest_from_list.py @@ -17,6 +17,7 @@ def post_message(self, message: SuggestionReady): self.log_list.append((message.suggestion, message.value)) +@pytest.mark.anyio async def test_first_suggestion_has_priority(): suggester = SuggestFromList(countries) @@ -24,6 +25,7 @@ async def test_first_suggestion_has_priority(): @pytest.mark.parametrize("value", ["s", "S", "sc", "sC", "Sc", "SC"]) +@pytest.mark.anyio async def test_case_insensitive_suggestions(value): suggester = SuggestFromList(countries, case_sensitive=False) log = [] @@ -47,6 +49,7 @@ async def test_case_insensitive_suggestions(value): "PORT", ], ) +@pytest.mark.anyio async def test_first_suggestion_has_priority_case_insensitive(value): suggester = SuggestFromList(countries, case_sensitive=False) log = [] diff --git a/tests/suggester/test_suggester.py b/tests/suggester/test_suggester.py index 91c80f7e82..04a9e07a88 100644 --- a/tests/suggester/test_suggester.py +++ b/tests/suggester/test_suggester.py @@ -21,6 +21,7 @@ def post_message(self, message: SuggestionReady): self.log_list.append((message.suggestion, message.value)) +@pytest.mark.anyio async def test_cache_on(): log = [] @@ -36,6 +37,7 @@ async def get_suggestion(self, value: str): assert log == ["hello"] +@pytest.mark.anyio async def test_cache_off(): log = [] @@ -51,6 +53,7 @@ async def get_suggestion(self, value: str): assert log == ["hello", "hello"] +@pytest.mark.anyio async def test_suggestion_ready_message(): log = [] suggester = FillSuggester() @@ -60,6 +63,7 @@ async def test_suggestion_ready_message(): assert log == [("helloxxxxx", "hello"), ("worldxxxxx", "world")] +@pytest.mark.anyio async def test_no_message_if_no_suggestion(): log = [] suggester = FillSuggester() @@ -67,6 +71,7 @@ async def test_no_message_if_no_suggestion(): assert log == [] +@pytest.mark.anyio async def test_suggestion_ready_message_on_cache_hit(): log = [] suggester = FillSuggester(use_cache=True) @@ -86,6 +91,7 @@ async def test_suggestion_ready_message_on_cache_hit(): "hELLO", ], ) +@pytest.mark.anyio async def test_case_insensitive_suggestions(value): class MySuggester(Suggester): async def get_suggestion(self, value: str): @@ -95,6 +101,7 @@ async def get_suggestion(self, value: str): await suggester._get_suggestion(DOMNode(), value) +@pytest.mark.anyio async def test_case_insensitive_cache_hits(): count = 0 diff --git a/tests/test_animation.py b/tests/test_animation.py index 5428f7a91a..bc19373efd 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -16,6 +16,7 @@ def compose(self) -> ComposeResult: yield Static("foo", id="foo") +@pytest.mark.anyio async def test_animate_height() -> None: """Test animating styles.height works.""" @@ -39,6 +40,7 @@ async def test_animate_height() -> None: assert static.styles.height.value == 100 +@pytest.mark.anyio async def test_scheduling_animation() -> None: """Test that scheduling an animation works.""" @@ -58,6 +60,7 @@ async def test_scheduling_animation() -> None: assert styles.background.rgb == (255, 255, 255) +@pytest.mark.anyio async def test_wait_for_current_animations() -> None: """Test that we can wait only for the current animations taking place.""" @@ -77,6 +80,7 @@ async def test_wait_for_current_animations() -> None: assert elapsed < (delay / 2) +@pytest.mark.anyio async def test_wait_for_current_and_scheduled_animations() -> None: """Test that we can wait for current and scheduled animations.""" @@ -95,6 +99,7 @@ async def test_wait_for_current_and_scheduled_animations() -> None: assert styles.background.rgb == (0, 0, 0) +@pytest.mark.anyio async def test_reverse_animations() -> None: """Test that you can create reverse animations. @@ -127,6 +132,7 @@ async def test_reverse_animations() -> None: assert styles.background.rgb == (0, 0, 0) +@pytest.mark.anyio async def test_schedule_reverse_animations() -> None: """Test that you can schedule reverse animations. @@ -171,6 +177,7 @@ def compose(self) -> ComposeResult: yield CancelAnimWidget() +@pytest.mark.anyio async def test_cancel_app_animation() -> None: """It should be possible to cancel a running app animation.""" @@ -182,6 +189,7 @@ async def test_cancel_app_animation() -> None: assert not pilot.app.animator.is_being_animated(pilot.app, "counter") +@pytest.mark.anyio async def test_cancel_app_non_animation() -> None: """It should be possible to attempt to cancel a non-running app animation.""" @@ -191,6 +199,7 @@ async def test_cancel_app_non_animation() -> None: assert not pilot.app.animator.is_being_animated(pilot.app, "counter") +@pytest.mark.anyio async def test_cancel_widget_animation() -> None: """It should be possible to cancel a running widget animation.""" @@ -203,6 +212,7 @@ async def test_cancel_widget_animation() -> None: assert not pilot.app.animator.is_being_animated(widget, "counter") +@pytest.mark.anyio async def test_cancel_widget_non_animation() -> None: """It should be possible to attempt to cancel a non-running widget animation.""" diff --git a/tests/test_animator.py b/tests/test_animator.py index a92e1ad6d8..841890f2e5 100644 --- a/tests/test_animator.py +++ b/tests/test_animator.py @@ -182,6 +182,7 @@ def _get_time(self): return self._time +@pytest.mark.anyio async def test_animator(): target = Mock() animator = MockAnimator(target) @@ -243,6 +244,7 @@ def test_bound_animator(): assert animator._animations[(id(animate_test), "foo")] == expected +@pytest.mark.anyio async def test_animator_on_complete_callback_not_fired_before_duration_ends(): callback = Mock() animate_test = AnimateTest() @@ -256,6 +258,7 @@ async def test_animator_on_complete_callback_not_fired_before_duration_ends(): assert not callback.called +@pytest.mark.anyio async def test_animator_on_complete_callback_fired_at_duration(): callback = Mock() animate_test = AnimateTest() diff --git a/tests/test_app.py b/tests/test_app.py index a986f2284a..c10f90e9fa 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -32,6 +32,7 @@ def compose(self) -> ComposeResult: yield Button("Click me!") +@pytest.mark.anyio async def test_hover_update_styles(): app = MyApp(ansi_color=False) async with app.run_test() as pilot: @@ -94,6 +95,7 @@ def test_setting_sub_title(): assert app.sub_title == "[True, False, 2]" +@pytest.mark.anyio async def test_default_return_code_is_zero(): app = App() async with app.run_test(): @@ -101,6 +103,7 @@ async def test_default_return_code_is_zero(): assert app.return_code == 0 +@pytest.mark.anyio async def test_return_code_is_one_after_crash(): class MyApp(App): def key_p(self): @@ -113,6 +116,7 @@ def key_p(self): assert app.return_code == 1 +@pytest.mark.anyio async def test_set_return_code(): app = App() async with app.run_test(): @@ -125,12 +129,14 @@ def test_no_return_code_before_running(): assert app.return_code is None +@pytest.mark.anyio async def test_no_return_code_while_running(): app = App() async with app.run_test(): assert app.return_code is None +@pytest.mark.anyio async def test_ansi_theme(): app = App() async with app.run_test(): @@ -152,6 +158,7 @@ async def test_ansi_theme(): assert app.ansi_theme == DIMMED_MONOKAI +@pytest.mark.anyio async def test_early_exit(): """Test exiting early doesn't cause issues.""" from textual.app import App @@ -184,6 +191,7 @@ def on_mount(self) -> None: app.run(inline=True, inline_no_clear=True) +@pytest.mark.anyio async def test_search_with_simple_commands(): """Test search with a list of SimpleCommands and ensure callbacks are invoked.""" called = False @@ -203,6 +211,7 @@ def callback(): assert called +@pytest.mark.anyio async def test_search_with_tuples(): """Test search with a list of tuples and ensure callbacks are invoked. In this case we also have no help text in the tuples. @@ -224,6 +233,7 @@ def callback(): assert called +@pytest.mark.anyio async def test_search_with_empty_list(): """Test search with an empty command list doesn't crash.""" app = App[None]() @@ -243,6 +253,7 @@ async def raw_click(pilot: Pilot, selector: str, times: int = 1): @pytest.mark.parametrize("number_of_clicks,final_count", [(1, 1), (2, 3), (3, 6)]) +@pytest.mark.anyio async def test_click_chain_initial_repeated_clicks( number_of_clicks: int, final_count: int ): @@ -268,6 +279,7 @@ def on_click(self, event: events.Click) -> None: assert click_count == final_count +@pytest.mark.anyio async def test_click_chain_different_offset(): click_count = 0 @@ -294,6 +306,7 @@ def on_click(self, event: events.Click) -> None: assert click_count == 3 +@pytest.mark.anyio async def test_click_chain_offset_changes_mid_chain(): """If we're in the middle of a click chain (e.g. we've double clicked), and the third click comes in at a different offset, that third click should be considered a single click. @@ -320,6 +333,7 @@ def on_click(self, event: events.Click) -> None: assert click_count == 1 +@pytest.mark.anyio async def test_click_chain_time_outwith_threshold(): click_count = 0 diff --git a/tests/test_app_focus_blur.py b/tests/test_app_focus_blur.py index bbeea8ebce..a78007f115 100644 --- a/tests/test_app_focus_blur.py +++ b/tests/test_app_focus_blur.py @@ -14,6 +14,7 @@ def compose(self) -> ComposeResult: yield Input(id=f"input-{n}") +@pytest.mark.anyio async def test_app_blur() -> None: """Test that AppBlur removes focus.""" async with FocusBlurApp().run_test() as pilot: @@ -24,6 +25,7 @@ async def test_app_blur() -> None: assert pilot.app.focused is None +@pytest.mark.anyio async def test_app_focus_restores_focus() -> None: """Test that AppFocus restores the correct focus.""" async with FocusBlurApp().run_test() as pilot: @@ -38,6 +40,7 @@ async def test_app_focus_restores_focus() -> None: assert pilot.app.focused.id == "input-4" +@pytest.mark.anyio async def test_app_focus_restores_none_focus() -> None: """Test that AppFocus doesn't set focus if nothing was focused.""" async with FocusBlurApp().run_test() as pilot: @@ -50,6 +53,7 @@ async def test_app_focus_restores_none_focus() -> None: assert pilot.app.focused is None +@pytest.mark.anyio async def test_app_focus_handles_missing_widget() -> None: """Test that AppFocus works even when the last-focused widget has gone away.""" async with FocusBlurApp().run_test() as pilot: @@ -64,6 +68,7 @@ async def test_app_focus_handles_missing_widget() -> None: assert pilot.app.focused is None +@pytest.mark.anyio async def test_app_focus_defers_to_new_focus() -> None: """Test that AppFocus doesn't undo a fresh focus done while the app is in AppBlur state.""" async with FocusBlurApp().run_test() as pilot: diff --git a/tests/test_await_remove.py b/tests/test_await_remove.py index 3b380e08b3..0feee820e3 100644 --- a/tests/test_await_remove.py +++ b/tests/test_await_remove.py @@ -13,6 +13,7 @@ def on_mount(self): self.mount(SelfRemovingLabel("I will remove myself!")) +@pytest.mark.anyio async def test_multiple_simultaneous_removals(): """Regression test for https://github.com/Textualize/textual/issues/2854.""" # The app should run and finish without raising any errors. diff --git a/tests/test_binding.py b/tests/test_binding.py index d11158f9f4..0d551693ac 100644 --- a/tests/test_binding.py +++ b/tests/test_binding.py @@ -105,6 +105,7 @@ class BrokenApp(App): BINDINGS = [(", ,", "foo", "Broken")] +@pytest.mark.anyio async def test_keymap_update() -> None: """Check that when keymaps are updated, the bindings_updated signal is sent.""" @@ -129,6 +130,7 @@ def signal_bindings_updated(screen: Screen) -> None: assert bindings_updated == [app.screen, app.screen] +@pytest.mark.anyio async def test_keymap_key() -> None: app: App[None] = App() diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index 69400f2230..d3f0d469f7 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -36,6 +36,7 @@ class NoBindings(App[None]): """An app with zero bindings.""" +@pytest.mark.anyio async def test_just_app_no_bindings() -> None: """An app with no bindings should have no bindings, other than the app's hard-coded ones.""" async with NoBindings().run_test() as pilot: @@ -62,6 +63,7 @@ class AlphaBinding(App[None]): BINDINGS = [Binding("a", "a", "a", priority=True)] +@pytest.mark.anyio async def test_just_app_alpha_binding() -> None: """An app with a single binding should have just the one binding.""" async with AlphaBinding().run_test() as pilot: @@ -86,6 +88,7 @@ class LowAlphaBinding(App[None]): BINDINGS = [Binding("a", "a", "a", priority=False)] +@pytest.mark.anyio async def test_just_app_low_priority_alpha_binding() -> None: """An app with a single low-priority binding should have just the one binding.""" async with LowAlphaBinding().run_test() as pilot: @@ -119,6 +122,7 @@ def on_mount(self) -> None: self.push_screen("main") +@pytest.mark.anyio async def test_app_screen_with_bindings() -> None: """Test a screen with a single key binding defined.""" async with AppWithScreenThatHasABinding().run_test() as pilot: @@ -148,6 +152,7 @@ def on_mount(self) -> None: self.push_screen("main") +@pytest.mark.anyio async def test_app_screen_with_low_bindings() -> None: """Test a screen with a single low-priority key binding defined.""" async with AppWithScreenThatHasALowBinding().run_test() as pilot: @@ -221,6 +226,7 @@ class AppWithMovementKeysBound(AppKeyRecorder): BINDINGS = AppKeyRecorder.make_bindings() +@pytest.mark.anyio async def test_pressing_alpha_on_app() -> None: """Test that pressing the alpha key, when it's bound on the app, results in an action fire.""" async with AppWithMovementKeysBound().run_test() as pilot: @@ -229,6 +235,7 @@ async def test_pressing_alpha_on_app() -> None: assert pilot.app.pressed_keys == [*AppKeyRecorder.ALPHAS] +@pytest.mark.anyio async def test_pressing_movement_keys_app() -> None: """Test that pressing the movement keys, when they're bound on the app, results in an action fire.""" async with AppWithMovementKeysBound().run_test() as pilot: @@ -267,6 +274,7 @@ def on_mount(self) -> None: self.query_one(FocusableWidgetWithBindings).focus() +@pytest.mark.anyio async def test_focused_child_widget_with_movement_bindings() -> None: """A focused child widget with movement bindings should handle its own actions.""" async with AppWithWidgetWithBindings().run_test() as pilot: @@ -314,6 +322,7 @@ def on_mount(self) -> None: self.push_screen("main") +@pytest.mark.anyio async def test_focused_child_widget_with_movement_bindings_on_screen() -> None: """A focused child widget, with movement bindings in the screen, should trigger screen actions.""" async with AppWithScreenWithBindingsWidgetNoBindings().run_test() as pilot: @@ -357,6 +366,7 @@ def on_mount(self) -> None: self.push_screen("main") +@pytest.mark.anyio async def test_contained_focused_child_widget_with_movement_bindings_on_screen() -> ( None ): @@ -398,6 +408,7 @@ def on_mount(self) -> None: self.query_one(WidgetWithBindingsNoInherit).focus() +@pytest.mark.anyio async def test_focused_child_widget_with_movement_bindings_no_inherit() -> None: """A focused child widget with movement bindings and inherit_bindings=False should handle its own actions.""" async with AppWithWidgetWithBindingsNoInherit().run_test() as pilot: @@ -450,6 +461,7 @@ def on_mount(self) -> None: self.push_screen("main") +@pytest.mark.anyio async def test_focused_child_widget_no_inherit_with_movement_bindings_on_screen() -> ( None ): @@ -507,6 +519,7 @@ def on_mount(self) -> None: self.push_screen("main") +@pytest.mark.anyio async def test_focused_child_widget_no_inherit_empty_bindings_with_movement_bindings_on_screen() -> ( None ): @@ -590,6 +603,7 @@ def on_mount(self) -> None: self.push_screen("main") +@pytest.mark.anyio async def test_overlapping_priority_bindings() -> None: """Test an app stack with overlapping bindings.""" async with PriorityOverlapApp().run_test() as pilot: @@ -605,6 +619,7 @@ async def test_overlapping_priority_bindings() -> None: ] +@pytest.mark.anyio async def test_skip_action() -> None: """Test that a binding may be skipped by an action raising SkipAction""" diff --git a/tests/test_border_subtitle.py b/tests/test_border_subtitle.py index 40d05beae8..585efc83b2 100644 --- a/tests/test_border_subtitle.py +++ b/tests/test_border_subtitle.py @@ -2,6 +2,7 @@ from textual.widget import Widget +@pytest.mark.anyio async def test_border_subtitle(): class BorderWidget(Widget): BORDER_TITLE = "foo" diff --git a/tests/test_call_x_schedulers.py b/tests/test_call_x_schedulers.py index 32f7ae1154..e9c80afcb7 100644 --- a/tests/test_call_x_schedulers.py +++ b/tests/test_call_x_schedulers.py @@ -14,6 +14,7 @@ def post_display_hook(self) -> None: self.display_count += 1 +@pytest.mark.anyio async def test_call_later() -> None: """Check that call later makes a call.""" app = CallLaterApp() @@ -24,6 +25,7 @@ async def test_call_later() -> None: await asyncio.wait_for(called_event.wait(), 1) +@pytest.mark.anyio async def test_call_after_refresh() -> None: """Check that call later makes a call after a refresh.""" app = CallLaterApp() diff --git a/tests/test_collapsible.py b/tests/test_collapsible.py index 5cbb973f3f..fc6c0f4399 100644 --- a/tests/test_collapsible.py +++ b/tests/test_collapsible.py @@ -16,6 +16,7 @@ def get_contents(collapsible: Collapsible) -> Collapsible.Contents: return collapsible.get_child_by_type(Collapsible.Contents) +@pytest.mark.anyio async def test_collapsible(): """It should be possible to access title and collapsed.""" collapsible = Collapsible(title="Pilot", collapsed=True) @@ -23,6 +24,7 @@ async def test_collapsible(): assert collapsible.collapsed +@pytest.mark.anyio async def test_compose_default_collapsible(): """Test default settings of Collapsible with 1 widget in contents.""" @@ -36,6 +38,7 @@ def compose(self) -> ComposeResult: assert len(get_contents(collapsible).children) == 1 +@pytest.mark.anyio async def test_compose_empty_collapsible(): """It should be possible to create an empty Collapsible.""" @@ -48,6 +51,7 @@ def compose(self) -> ComposeResult: assert len(get_contents(collapsible).children) == 0 +@pytest.mark.anyio async def test_compose_nested_collapsible(): """Children Collapsibles are independent from parents Collapsibles.""" @@ -63,6 +67,7 @@ def compose(self) -> ComposeResult: assert not inner.collapsed +@pytest.mark.anyio async def test_compose_expanded_collapsible(): """It should be possible to create a Collapsible with expanded contents.""" @@ -76,6 +81,7 @@ def compose(self) -> ComposeResult: assert not get_contents(collapsible).has_class(COLLAPSED_CLASS) +@pytest.mark.anyio async def test_collapsible_collapsed_contents_display_false(): """Test default settings of Collapsible with 1 widget in contents.""" @@ -88,6 +94,7 @@ def compose(self) -> ComposeResult: assert not get_contents(collapsible).display +@pytest.mark.anyio async def test_collapsible_expanded_contents_display_true(): """Test default settings of Collapsible with 1 widget in contents.""" @@ -100,6 +107,7 @@ def compose(self) -> ComposeResult: assert get_contents(collapsible).display +@pytest.mark.anyio async def test_toggle_title(): """Clicking title should update ``collapsed``.""" @@ -118,6 +126,7 @@ def compose(self) -> ComposeResult: assert not collapsible.collapsed +@pytest.mark.anyio async def test_toggle_message(): """Toggling should post a message.""" @@ -147,6 +156,7 @@ def catch_collapsible_events(self) -> None: assert len(hits) == 2 +@pytest.mark.anyio async def test_expand_message(): """Clicking to expand should post a message.""" @@ -169,6 +179,7 @@ def on_collapsible_expanded(self) -> None: assert len(hits) == 1 +@pytest.mark.anyio async def test_expand_via_watcher_message(): """Setting `collapsed` to `False` should post a message.""" @@ -191,6 +202,7 @@ def on_collapsible_expanded(self) -> None: assert len(hits) == 1 +@pytest.mark.anyio async def test_collapse_message(): """Clicking on collapsed should post a message.""" @@ -213,6 +225,7 @@ def on_collapsible_collapsed(self) -> None: assert len(hits) == 1 +@pytest.mark.anyio async def test_collapse_via_watcher_message(): """Setting `collapsed` to `True` should post a message.""" @@ -235,6 +248,7 @@ def on_collapsible_collapsed(self) -> None: assert len(hits) == 1 +@pytest.mark.anyio async def test_collapsible_title_reactive_change(): class CollapsibleApp(App[None]): def compose(self) -> ComposeResult: diff --git a/tests/test_command.py b/tests/test_command.py index c0a9e49b9e..61752a9e2f 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -51,6 +51,7 @@ def check_quit(quit: bool | None) -> None: self.push_screen(QuitScreen(), check_quit) +@pytest.mark.anyio async def test_command_dismiss(): """Regression test for https://github.com/Textualize/textual/issues/5512""" app = ModalApp() diff --git a/tests/test_compositor.py b/tests/test_compositor.py index 3717d07de5..92dc985312 100644 --- a/tests/test_compositor.py +++ b/tests/test_compositor.py @@ -3,6 +3,7 @@ from textual.widgets import Static +@pytest.mark.anyio async def test_compositor_scroll_placements(): """Regression test for https://github.com/Textualize/textual/issues/5249 The Static should remain visible. diff --git a/tests/test_containers.py b/tests/test_containers.py index 52fddf1a71..6f63ce6991 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -12,6 +12,7 @@ from textual.widgets import Label +@pytest.mark.anyio async def test_horizontal_vs_horizontalscroll_scrolling(): """Check the default scrollbar behaviours for `Horizontal` and `HorizontalScroll`.""" @@ -41,6 +42,7 @@ def compose(self) -> ComposeResult: assert horizontal_scroll.scrollbars_enabled == (False, True) +@pytest.mark.anyio async def test_vertical_vs_verticalscroll_scrolling(): """Check the default scrollbar behaviours for `Vertical` and `VerticalScroll`.""" @@ -70,6 +72,7 @@ def compose(self) -> ComposeResult: assert vertical_scroll.scrollbars_enabled == (True, False) +@pytest.mark.anyio async def test_center_container(): """Check the size of the container `Center`.""" @@ -85,6 +88,7 @@ def compose(self) -> ComposeResult: assert center.size.height == 3 +@pytest.mark.anyio async def test_middle_container(): """Check the size of the container `Middle`.""" @@ -100,6 +104,7 @@ def compose(self) -> ComposeResult: assert middle.size.height == app.size.height +@pytest.mark.anyio async def test_scrollbar_zero_thickness(): """Ensuring that scrollbars can be set to zero thickness.""" diff --git a/tests/test_content_switcher.py b/tests/test_content_switcher.py index cc1e37a3e9..526690faea 100644 --- a/tests/test_content_switcher.py +++ b/tests/test_content_switcher.py @@ -19,6 +19,7 @@ def compose(self) -> ComposeResult: yield Widget(id=f"w{n}") +@pytest.mark.anyio async def test_no_initial_display() -> None: """Test starting a content switcher with nothing shown.""" async with SwitcherApp().run_test() as pilot: @@ -29,6 +30,7 @@ async def test_no_initial_display() -> None: assert pilot.app.query_one(ContentSwitcher).visible_content is None +@pytest.mark.anyio async def test_initial_display() -> None: """Test starting a content switcher with a widget initially shown.""" async with SwitcherApp("w3").run_test() as pilot: @@ -40,6 +42,7 @@ async def test_initial_display() -> None: ).visible_content is pilot.app.query_one("#w3") +@pytest.mark.anyio async def test_no_initial_display_then_set() -> None: """Test starting a content switcher with nothing shown then setting the display.""" async with SwitcherApp().run_test() as pilot: @@ -57,6 +60,7 @@ async def test_no_initial_display_then_set() -> None: ).visible_content is pilot.app.query_one("#w3") +@pytest.mark.anyio async def test_initial_display_then_change() -> None: """Test starting a content switcher with a widget initially shown then changing it.""" async with SwitcherApp("w3").run_test() as pilot: @@ -75,6 +79,7 @@ async def test_initial_display_then_change() -> None: ).visible_content is pilot.app.query_one("#w2") +@pytest.mark.anyio async def test_initial_display_then_hide() -> None: """Test starting a content switcher with a widget initially shown then hide all.""" async with SwitcherApp("w3").run_test() as pilot: @@ -91,6 +96,7 @@ async def test_initial_display_then_hide() -> None: @pytest.mark.xfail( reason="The expected exception doesn't appear to make it to pytest -- perhaps related to https://github.com/Textualize/textual/issues/1972" ) +@pytest.mark.anyio async def test_initial_display_unknown_id() -> None: """Test setting an initial display to an unknown widget ID.""" with pytest.raises(NoMatches): @@ -98,6 +104,7 @@ async def test_initial_display_unknown_id() -> None: pass +@pytest.mark.anyio async def test_set_current_to_unknown_id() -> None: """Test attempting to switch to an unknown widget ID.""" async with SwitcherApp().run_test() as pilot: @@ -109,6 +116,7 @@ async def test_set_current_to_unknown_id() -> None: pilot.app.query_one(ContentSwitcher).current = "does-not-exist" +@pytest.mark.anyio async def test_add_content() -> None: async with SwitcherApp().run_test() as pilot: switcher = pilot.app.query_one(ContentSwitcher) diff --git a/tests/test_data_bind.py b/tests/test_data_bind.py index a6d86dce25..83b1b9f5c7 100644 --- a/tests/test_data_bind.py +++ b/tests/test_data_bind.py @@ -20,6 +20,7 @@ def compose(self) -> ComposeResult: yield FooLabel(id="label2") # Not bound +@pytest.mark.anyio async def test_data_binding(): app = DataBindApp() async with app.run_test() as pilot: @@ -70,6 +71,7 @@ async def test_data_binding(): assert label2.foo == "Spam" +@pytest.mark.anyio async def test_data_binding_missing_reactive(): class DataBindErrorApp(App): diff --git a/tests/test_data_table.py b/tests/test_data_table.py index d78d49e2fa..59761c1997 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -63,6 +63,7 @@ async def _on_message(self, message: Message) -> None: self.record_data_table_event(message) +@pytest.mark.anyio async def test_datatable_message_emission(): app = DataTableApp() expected_messages = [] @@ -168,6 +169,7 @@ async def test_datatable_message_emission(): assert app.message_names == expected_messages +@pytest.mark.anyio async def test_empty_table_interactions(): app = DataTableApp() async with app.run_test() as pilot: @@ -178,6 +180,7 @@ async def test_empty_table_interactions(): @pytest.mark.parametrize("show_header", [True, False]) +@pytest.mark.anyio async def test_cursor_movement_with_home_pagedown_etc(show_header): app = DataTableApp() @@ -203,6 +206,7 @@ async def test_cursor_movement_with_home_pagedown_etc(show_header): assert table.cursor_coordinate == Coordinate(0, 1) +@pytest.mark.anyio async def test_add_rows(): app = DataTableApp() async with app.run_test(): @@ -217,6 +221,7 @@ async def test_add_rows(): assert all(key in table._data for key in row_keys) +@pytest.mark.anyio async def test_add_rows_user_defined_keys(): app = DataTableApp() async with app.run_test(): @@ -247,6 +252,7 @@ async def test_add_rows_user_defined_keys(): assert table.rows["algernon"] == first_row +@pytest.mark.anyio async def test_add_row_duplicate_key(): app = DataTableApp() async with app.run_test(): @@ -257,6 +263,7 @@ async def test_add_row_duplicate_key(): table.add_row("2", key="1") # Duplicate row key +@pytest.mark.anyio async def test_add_row_too_many_values(): app = DataTableApp() async with app.run_test(): @@ -267,6 +274,7 @@ async def test_add_row_too_many_values(): table.add_row("1", "2") +@pytest.mark.anyio async def test_add_column_duplicate_key(): app = DataTableApp() async with app.run_test(): @@ -276,6 +284,7 @@ async def test_add_column_duplicate_key(): table.add_column("B", key="A") # Duplicate column key +@pytest.mark.anyio async def test_add_column_with_width(): app = DataTableApp() async with app.run_test(): @@ -290,6 +299,7 @@ async def test_add_column_with_width(): ) +@pytest.mark.anyio async def test_add_columns(): app = DataTableApp() async with app.run_test(): @@ -299,6 +309,7 @@ async def test_add_columns(): assert len(table.columns) == 3 +@pytest.mark.anyio async def test_add_columns_user_defined_keys(): app = DataTableApp() async with app.run_test(): @@ -308,6 +319,7 @@ async def test_add_columns_user_defined_keys(): assert key == key +@pytest.mark.anyio async def test_remove_row(): app = DataTableApp() async with app.run_test(): @@ -321,6 +333,7 @@ async def test_remove_row(): assert len(table.rows) == 2 +@pytest.mark.anyio async def test_remove_row_and_update(): """Regression test for https://github.com/Textualize/textual/issues/3470 - Crash when attempting to remove and update the same cell.""" @@ -334,6 +347,7 @@ async def test_remove_row_and_update(): await pilot.pause() +@pytest.mark.anyio async def test_remove_column(): app = DataTableApp() async with app.run_test(): @@ -348,6 +362,7 @@ async def test_remove_column(): assert table.get_row_at(2) == ["2/1"] +@pytest.mark.anyio async def test_remove_column_and_update(): """Regression test for https://github.com/Textualize/textual/issues/3470 - Crash when attempting to remove and update the same cell.""" @@ -361,6 +376,7 @@ async def test_remove_column_and_update(): await pilot.pause() +@pytest.mark.anyio async def test_clear(): app = DataTableApp() async with app.run_test(): @@ -393,6 +409,7 @@ async def test_clear(): assert len(table.columns) == 0 +@pytest.mark.anyio async def test_column_labels() -> None: app = DataTableApp() async with app.run_test(): @@ -403,6 +420,7 @@ async def test_column_labels() -> None: assert actual_labels == expected_labels +@pytest.mark.anyio async def test_initial_column_widths() -> None: app = DataTableApp() async with app.run_test(): @@ -422,6 +440,7 @@ async def test_initial_column_widths() -> None: assert table.columns[bar].content_width == 6 +@pytest.mark.anyio async def test_get_cell_returns_value_at_cell(): app = DataTableApp() async with app.run_test(): @@ -431,6 +450,7 @@ async def test_get_cell_returns_value_at_cell(): assert table.get_cell("R1", "C1") == "TargetValue" +@pytest.mark.anyio async def test_get_cell_invalid_row_key(): app = DataTableApp() async with app.run_test(): @@ -441,6 +461,7 @@ async def test_get_cell_invalid_row_key(): table.get_cell("INVALID_ROW", "C1") +@pytest.mark.anyio async def test_get_cell_invalid_column_key(): app = DataTableApp() async with app.run_test(): @@ -451,6 +472,7 @@ async def test_get_cell_invalid_column_key(): table.get_cell("R1", "INVALID_COLUMN") +@pytest.mark.anyio async def test_get_cell_coordinate_returns_coordinate(): app = DataTableApp() async with app.run_test(): @@ -469,6 +491,7 @@ async def test_get_cell_coordinate_returns_coordinate(): assert table.get_cell_coordinate("R3", "C2") == Coordinate(2, 1) +@pytest.mark.anyio async def test_get_cell_coordinate_invalid_row_key(): app = DataTableApp() async with app.run_test(): @@ -480,6 +503,7 @@ async def test_get_cell_coordinate_invalid_row_key(): coordinate = table.get_cell_coordinate("INVALID_ROW", "C1") +@pytest.mark.anyio async def test_get_cell_coordinate_invalid_column_key(): app = DataTableApp() async with app.run_test(): @@ -491,6 +515,7 @@ async def test_get_cell_coordinate_invalid_column_key(): coordinate = table.get_cell_coordinate("R1", "INVALID_COLUMN") +@pytest.mark.anyio async def test_get_cell_at_returns_value_at_cell(): app = DataTableApp() async with app.run_test(): @@ -500,6 +525,7 @@ async def test_get_cell_at_returns_value_at_cell(): assert table.get_cell_at(Coordinate(0, 0)) == "0/0" +@pytest.mark.anyio async def test_get_cell_at_exception(): app = DataTableApp() async with app.run_test(): @@ -510,6 +536,7 @@ async def test_get_cell_at_exception(): table.get_cell_at(Coordinate(9999, 0)) +@pytest.mark.anyio async def test_get_row(): app = DataTableApp() async with app.run_test(): @@ -526,6 +553,7 @@ async def test_get_row(): assert table.get_row(second_row) == [3, 2, 1] +@pytest.mark.anyio async def test_get_row_invalid_row_key(): app = DataTableApp() async with app.run_test(): @@ -534,6 +562,7 @@ async def test_get_row_invalid_row_key(): table.get_row("INVALID") +@pytest.mark.anyio async def test_get_row_at(): app = DataTableApp() async with app.run_test(): @@ -553,6 +582,7 @@ async def test_get_row_at(): @pytest.mark.parametrize("index", (-1, 2)) +@pytest.mark.anyio async def test_get_row_at_invalid_index(index): app = DataTableApp() async with app.run_test(): @@ -564,6 +594,7 @@ async def test_get_row_at_invalid_index(index): table.get_row_at(index) +@pytest.mark.anyio async def test_get_row_index_returns_index(): app = DataTableApp() async with app.run_test(): @@ -579,6 +610,7 @@ async def test_get_row_index_returns_index(): assert table.get_row_index("R3") == 2 +@pytest.mark.anyio async def test_get_row_index_invalid_row_key(): app = DataTableApp() async with app.run_test(): @@ -590,6 +622,7 @@ async def test_get_row_index_invalid_row_key(): index = table.get_row_index("InvalidRow") +@pytest.mark.anyio async def test_get_column(): app = DataTableApp() async with app.run_test(): @@ -604,6 +637,7 @@ async def test_get_column(): next(cells) +@pytest.mark.anyio async def test_get_column_invalid_key(): app = DataTableApp() async with app.run_test(): @@ -612,6 +646,7 @@ async def test_get_column_invalid_key(): list(table.get_column("INVALID")) +@pytest.mark.anyio async def test_get_column_at(): app = DataTableApp() async with app.run_test(): @@ -627,6 +662,7 @@ async def test_get_column_at(): @pytest.mark.parametrize("index", [-1, 5]) +@pytest.mark.anyio async def test_get_column_at_invalid_index(index): app = DataTableApp() async with app.run_test(): @@ -635,6 +671,7 @@ async def test_get_column_at_invalid_index(index): list(table.get_column_at(index)) +@pytest.mark.anyio async def test_get_column_index_returns_index(): app = DataTableApp() async with app.run_test(): @@ -650,6 +687,7 @@ async def test_get_column_index_returns_index(): assert table.get_column_index("C3") == 2 +@pytest.mark.anyio async def test_get_column_index_invalid_column_key(): app = DataTableApp() async with app.run_test(): @@ -663,6 +701,7 @@ async def test_get_column_index_invalid_column_key(): index = table.get_column_index("InvalidCol") +@pytest.mark.anyio async def test_update_cell_cell_exists(): app = DataTableApp() async with app.run_test(): @@ -673,6 +712,7 @@ async def test_update_cell_cell_exists(): assert table.get_cell("1", "A") == "NEW_VALUE" +@pytest.mark.anyio async def test_update_cell_cell_doesnt_exist(): app = DataTableApp() async with app.run_test(): @@ -683,6 +723,7 @@ async def test_update_cell_cell_doesnt_exist(): table.update_cell("INVALID", "CELL", "Value") +@pytest.mark.anyio async def test_update_cell_invalid_column_key(): """Regression test for https://github.com/Textualize/textual/issues/3335""" app = DataTableApp() @@ -694,6 +735,7 @@ async def test_update_cell_invalid_column_key(): table.update_cell("R1", "INVALID_COLUMN", "New Value") +@pytest.mark.anyio async def test_update_cell_at_coordinate_exists(): app = DataTableApp() async with app.run_test(): @@ -705,6 +747,7 @@ async def test_update_cell_at_coordinate_exists(): assert table.get_cell(row_0, column_1) == "newvalue" +@pytest.mark.anyio async def test_update_cell_at_coordinate_doesnt_exist(): app = DataTableApp() async with app.run_test(): @@ -728,6 +771,7 @@ async def test_update_cell_at_coordinate_doesnt_exist(): ("12345", "123456789", 9), ], ) +@pytest.mark.anyio async def test_update_cell_at_column_width(label, new_value, new_content_width): # Initial cell values are length 3. Let's update cell content and ensure # that the width of the column is correct given the new cell content widths @@ -748,6 +792,7 @@ async def test_update_cell_at_column_width(label, new_value, new_content_width): ) +@pytest.mark.anyio async def test_coordinate_to_cell_key(): app = DataTableApp() async with app.run_test(): @@ -759,6 +804,7 @@ async def test_coordinate_to_cell_key(): assert cell_key == CellKey(row_key, column_key) +@pytest.mark.anyio async def test_coordinate_to_cell_key_invalid_coordinate(): app = DataTableApp() async with app.run_test(): @@ -767,6 +813,7 @@ async def test_coordinate_to_cell_key_invalid_coordinate(): table.coordinate_to_cell_key(Coordinate(9999, 9999)) +@pytest.mark.anyio async def test_datatable_click_cell_cursor(): """When the cell cursor is used, and we click, we emit a CellHighlighted *and* a CellSelected message for the cell that was clicked. @@ -796,6 +843,7 @@ async def test_datatable_click_cell_cursor(): assert cell_selected_event.coordinate == Coordinate(1, 0) +@pytest.mark.anyio async def test_click_row_cursor(): """When the row cursor is used, and we click, we emit a RowHighlighted *and* a RowSelected message for the row that was clicked.""" @@ -819,6 +867,7 @@ async def test_click_row_cursor(): assert row_highlighted.cursor_row == 1 +@pytest.mark.anyio async def test_click_column_cursor(): """When the column cursor is used, and we click, we emit a ColumnHighlighted *and* a ColumnSelected message for the column that was clicked.""" @@ -844,6 +893,7 @@ async def test_click_column_cursor(): assert column_highlighted.cursor_column == 0 +@pytest.mark.anyio async def test_hover_coordinate(): """Ensure that the hover_coordinate reactive is updated as expected.""" app = DataTableApp() @@ -858,6 +908,7 @@ async def test_hover_coordinate(): assert table.hover_coordinate == Coordinate(1, 0) +@pytest.mark.anyio async def test_hover_mouse_leave(): """When the mouse cursor leaves the DataTable, there should be no hover highlighting.""" app = DataTableApp() @@ -875,6 +926,7 @@ async def test_hover_mouse_leave(): assert not table._show_hover_cursor +@pytest.mark.anyio async def test_header_selected(): """Ensure that a HeaderSelected event gets posted when we click on the header in the DataTable.""" @@ -898,6 +950,7 @@ async def test_header_selected(): assert app.message_names.count("HeaderSelected") == 1 +@pytest.mark.anyio async def test_row_label_selected(): """Ensure that the DataTable sends a RowLabelSelected event when the user clicks on a row label.""" @@ -919,6 +972,7 @@ async def test_row_label_selected(): assert app.message_names.count("RowLabelSelected") == 1 +@pytest.mark.anyio async def test_sort_coordinate_and_key_access(): """Ensure that, after sorting, that coordinates and cell keys can still be used to retrieve the correct cell.""" @@ -952,6 +1006,7 @@ async def test_sort_coordinate_and_key_access(): assert table.ordered_rows[2].key == row_three +@pytest.mark.anyio async def test_sort_reverse_coordinate_and_key_access(): """Ensure that, after sorting, that coordinates and cell keys can still be used to retrieve the correct cell.""" @@ -985,6 +1040,7 @@ async def test_sort_reverse_coordinate_and_key_access(): assert table.ordered_rows[2].key == row_one +@pytest.mark.anyio async def test_cell_cursor_highlight_events(): app = DataTableApp() async with app.run_test() as pilot: @@ -1021,6 +1077,7 @@ async def test_cell_cursor_highlight_events(): assert latest_message.cell_key == CellKey(row_two_key, column_two_key) +@pytest.mark.anyio async def test_row_cursor_highlight_events(): app = DataTableApp() async with app.run_test() as pilot: @@ -1057,6 +1114,7 @@ async def test_row_cursor_highlight_events(): assert latest_message.cursor_row == 0 +@pytest.mark.anyio async def test_column_cursor_highlight_events(): app = DataTableApp() async with app.run_test() as pilot: @@ -1095,6 +1153,7 @@ async def test_column_cursor_highlight_events(): assert latest_message.cursor_column == 0 +@pytest.mark.anyio async def test_reuse_row_key_after_clear(): """Regression test for https://github.com/Textualize/textual/issues/1806""" app = DataTableApp() @@ -1110,6 +1169,7 @@ async def test_reuse_row_key_after_clear(): assert table.get_row("ROW2") == [7, 8] +@pytest.mark.anyio async def test_reuse_column_key_after_clear(): """Regression test for https://github.com/Textualize/textual/issues/1806""" app = DataTableApp() @@ -1160,6 +1220,7 @@ def test_key_string_lookup(): assert dictionary[RowKey("hello")] == "world" +@pytest.mark.anyio async def test_scrolling_cursor_into_view(): """Regression test for https://github.com/Textualize/textual/issues/2459""" @@ -1182,6 +1243,7 @@ def key_c(self): assert table.scroll_y > 100 +@pytest.mark.anyio async def test_move_cursor(): app = DataTableApp() @@ -1198,6 +1260,7 @@ async def test_move_cursor(): assert table.cursor_coordinate == Coordinate(3, 3) +@pytest.mark.anyio async def test_unset_hover_highlight_when_no_table_cell_under_mouse(): """When there isn't a table cell under the mouse cursor, there should be no hover highlighting. @@ -1220,6 +1283,7 @@ async def test_unset_hover_highlight_when_no_table_cell_under_mouse(): assert not table._show_hover_cursor +@pytest.mark.anyio async def test_sort_by_all_columns_no_key(): """Test sorting a `DataTable` by all columns.""" @@ -1245,6 +1309,7 @@ async def test_sort_by_all_columns_no_key(): assert table.get_row_at(2) == [1, 1, 9] +@pytest.mark.anyio async def test_sort_by_multiple_columns_no_key(): """Test sorting a `DataTable` by multiple columns.""" @@ -1277,6 +1342,7 @@ async def test_sort_by_multiple_columns_no_key(): assert table.get_row_at(2) == [2, 9, 5] +@pytest.mark.anyio async def test_sort_by_function_sum(): """Test sorting a `DataTable` using a custom sort function.""" @@ -1324,6 +1390,7 @@ def custom_sort(row_data): ("1\n2\n3\n4\n5\n6\n7", 7), ], ) +@pytest.mark.anyio async def test_add_row_auto_height(cell: RenderableType, height: int): app = DataTableApp() async with app.run_test() as pilot: @@ -1335,6 +1402,7 @@ async def test_add_row_auto_height(cell: RenderableType, height: int): assert row.height == height +@pytest.mark.anyio async def test_add_row_expands_column_widths(): """Regression test for https://github.com/Textualize/textual/issues/1026.""" app = DataTableApp() @@ -1365,6 +1433,7 @@ async def test_add_row_expands_column_widths(): ) +@pytest.mark.anyio async def test_cell_padding_updates_virtual_size(): app = DataTableApp() @@ -1386,6 +1455,7 @@ async def test_cell_padding_updates_virtual_size(): assert width + 13 * 2 * 3 == table.virtual_size.width +@pytest.mark.anyio async def test_cell_padding_cannot_be_negative(): app = DataTableApp() async with app.run_test(): @@ -1396,6 +1466,7 @@ async def test_cell_padding_cannot_be_negative(): assert table.cell_padding == 0 +@pytest.mark.anyio async def test_move_cursor_respects_animate_parameter(): """Regression test for https://github.com/Textualize/textual/issues/3840 @@ -1434,6 +1505,7 @@ def key_s(self): assert scrolls == [True, False] +@pytest.mark.anyio async def test_clicking_border_link_doesnt_crash(): """Regression test for https://github.com/Textualize/textual/issues/4410""" diff --git a/tests/test_demo.py b/tests/test_demo.py index a40de63401..7c9a8faee3 100644 --- a/tests/test_demo.py +++ b/tests/test_demo.py @@ -1,6 +1,7 @@ from textual.demo.demo_app import DemoApp +@pytest.mark.anyio async def test_demo(): """Test the demo runs.""" # Test he demo can at least run. diff --git a/tests/test_disabled.py b/tests/test_disabled.py index 658fd36614..690ee95e67 100644 --- a/tests/test_disabled.py +++ b/tests/test_disabled.py @@ -45,6 +45,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_all_initially_enabled() -> None: """All widgets should start out enabled.""" async with DisableApp().run_test() as pilot: @@ -53,6 +54,7 @@ async def test_all_initially_enabled() -> None: ) +@pytest.mark.anyio async def test_enabled_widgets_have_enabled_pseudo_class() -> None: """All enabled widgets should have the :enabled pseudoclass.""" async with DisableApp().run_test() as pilot: @@ -62,6 +64,7 @@ async def test_enabled_widgets_have_enabled_pseudo_class() -> None: ) +@pytest.mark.anyio async def test_all_individually_disabled() -> None: """Post-disable all widgets should report being disabled.""" async with DisableApp().run_test() as pilot: @@ -72,6 +75,7 @@ async def test_all_individually_disabled() -> None: ) +@pytest.mark.anyio async def test_disabled_widgets_have_disabled_pseudo_class() -> None: """All disabled widgets should have the :disabled pseudoclass.""" async with DisableApp().run_test() as pilot: @@ -83,6 +87,7 @@ async def test_disabled_widgets_have_disabled_pseudo_class() -> None: ) +@pytest.mark.anyio async def test_disable_via_container() -> None: """All child widgets should appear (to CSS) as disabled by a container being disabled.""" async with DisableApp().run_test() as pilot: @@ -137,6 +142,7 @@ def on_mount(self): Switch, ], ) +@pytest.mark.anyio async def test_children_loses_focus_if_container_is_disabled(widget): """Regression test for https://github.com/Textualize/textual/issues/2772.""" app = ChildrenNoFocusDisabledContainer() diff --git a/tests/test_driver.py b/tests/test_driver.py index 764c9930da..878b0a0401 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -4,6 +4,7 @@ from textual.widgets import Button +@pytest.mark.anyio async def test_driver_mouse_down_up_click(): """Mouse down and up should issue a click.""" @@ -27,6 +28,7 @@ def handle(self, event): assert isinstance(app.messages[2], Click) +@pytest.mark.anyio async def test_driver_mouse_down_up_click_widget(): """Mouse down and up should issue a click when they're on a widget.""" @@ -47,6 +49,7 @@ def on_button_pressed(self, event): assert len(app.messages) == 1 +@pytest.mark.anyio async def test_driver_mouse_down_drag_inside_widget_up_click(): """Mouse down and up should issue a click, even if the mouse moves but remains inside the same widget.""" @@ -88,6 +91,7 @@ def on_button_pressed(self, event): assert len(app.messages) == 1 +@pytest.mark.anyio async def test_driver_mouse_down_drag_outside_widget_up_click(): """Mouse down and up don't issue a click if the mouse moves outside of the initial widget.""" diff --git a/tests/test_dynamic_bindings.py b/tests/test_dynamic_bindings.py index 05b9baecd3..20bb2b04e6 100644 --- a/tests/test_dynamic_bindings.py +++ b/tests/test_dynamic_bindings.py @@ -3,6 +3,7 @@ from textual.app import App +@pytest.mark.anyio async def test_dynamic_disabled(): """Check we can dynamically disable bindings.""" actions = [] diff --git a/tests/test_file_monitor.py b/tests/test_file_monitor.py index fc93c7d292..a3d23558be 100644 --- a/tests/test_file_monitor.py +++ b/tests/test_file_monitor.py @@ -15,6 +15,7 @@ def test_file_never_found(): file_monitor.check() # Ensuring no exceptions are raised. +@pytest.mark.anyio async def test_file_deletion(tmp_path): """In some environments, a file can become temporarily unavailable during saving. diff --git a/tests/test_focus.py b/tests/test_focus.py index 98698c3b42..c0fb6e3439 100644 --- a/tests/test_focus.py +++ b/tests/test_focus.py @@ -31,6 +31,7 @@ def compose(self) -> ComposeResult: yield ChildrenFocusableOnly(Focusable(id="child", classes="c")) +@pytest.mark.anyio async def test_focus_chain(): app = App() async with app.run_test(): @@ -52,6 +53,7 @@ async def test_focus_chain(): assert focus_chain == ["foo", "container1", "Paul", "baz", "child"] +@pytest.mark.anyio async def test_allow_focus(): """Test allow_focus and allow_focus_children are called and the result used.""" focusable_allow_focus_called = False @@ -91,6 +93,7 @@ def allow_focus_children(self) -> bool: assert non_focusable_allow_focus_called +@pytest.mark.anyio async def test_focus_next_and_previous(): app = FocusTestApp() async with app.run_test(): @@ -108,6 +111,7 @@ async def test_focus_next_and_previous(): assert screen.focus_previous().id == "foo" +@pytest.mark.anyio async def test_focus_next_wrap_around(): """Ensure focusing the next widget wraps around the focus chain.""" app = FocusTestApp() @@ -120,6 +124,7 @@ async def test_focus_next_wrap_around(): assert screen.focus_next().id == "foo" +@pytest.mark.anyio async def test_focus_previous_wrap_around(): """Ensure focusing the previous widget wraps around the focus chain.""" app = FocusTestApp() @@ -132,6 +137,7 @@ async def test_focus_previous_wrap_around(): assert screen.focus_previous().id == "child" +@pytest.mark.anyio async def test_wrap_around_selector(): """Ensure moving focus in both directions wraps around the focus chain.""" app = FocusTestApp() @@ -145,6 +151,7 @@ async def test_wrap_around_selector(): assert screen.focus_next("#foo").id == "foo" +@pytest.mark.anyio async def test_no_focus_empty_selector(): """Ensure focus is cleared when selector matches nothing.""" app = FocusTestApp() @@ -165,6 +172,7 @@ async def test_no_focus_empty_selector(): assert screen.focused is None +@pytest.mark.anyio async def test_focus_next_and_previous_with_type_selector(): """Move focus with a selector that matches the currently focused node.""" app = FocusTestApp() @@ -183,6 +191,7 @@ async def test_focus_next_and_previous_with_type_selector(): assert screen.focus_previous(Focusable).id == "foo" +@pytest.mark.anyio async def test_focus_next_and_previous_with_str_selector(): """Move focus with a selector that matches the currently focused node.""" app = FocusTestApp() @@ -200,6 +209,7 @@ async def test_focus_next_and_previous_with_str_selector(): assert screen.focus_previous(".a").id == "foo" +@pytest.mark.anyio async def test_focus_next_and_previous_with_type_selector_without_self(): """Test moving the focus with a selector that does not match the currently focused node.""" app = App() @@ -244,6 +254,7 @@ async def test_focus_next_and_previous_with_type_selector_without_self(): assert screen.focus_previous(Input).id == "w5" +@pytest.mark.anyio async def test_focus_next_and_previous_with_str_selector_without_self(): """Test moving the focus with a selector that does not match the currently focused node.""" app = FocusTestApp() @@ -262,6 +273,7 @@ async def test_focus_next_and_previous_with_str_selector_without_self(): assert screen.focus_previous(".b").id == "baz" +@pytest.mark.anyio async def test_focus_does_not_move_to_invisible_widgets(): """Make sure invisible widgets don't get focused by accident. @@ -283,6 +295,7 @@ def compose(self): assert app.screen.focus_next().id == "three" +@pytest.mark.anyio async def test_focus_moves_to_visible_widgets_inside_invisible_containers(): """Regression test for https://github.com/Textualize/textual/issues/3053.""" @@ -303,6 +316,7 @@ def compose(self): assert app.screen.focus_next().id == "three" +@pytest.mark.anyio async def test_focus_chain_handles_inherited_visibility(): """Regression test for https://github.com/Textualize/textual/issues/3053 @@ -368,6 +382,7 @@ def compose(self): ] +@pytest.mark.anyio async def test_mouse_down_gives_focus(): class MyApp(App): AUTO_FOCUS = None @@ -384,6 +399,7 @@ def compose(self): assert isinstance(app.focused, Button) +@pytest.mark.anyio async def test_mouse_up_does_not_give_focus(): class MyApp(App): AUTO_FOCUS = None @@ -400,6 +416,7 @@ def compose(self): assert app.focused is None +@pytest.mark.anyio async def test_focus_pseudo_class(): """Test focus and blue pseudo classes""" @@ -428,6 +445,7 @@ def compose(self) -> ComposeResult: assert "focus" in classes +@pytest.mark.anyio async def test_get_focusable_widget_at() -> None: """Check that clicking a non-focusable widget will focus any (focusable) ancestors.""" @@ -467,6 +485,7 @@ def compose(self) -> ComposeResult: assert app.screen.focused is None +@pytest.mark.anyio async def test_allow_focus_override(): """Test that allow_focus() method override can_focus.""" diff --git a/tests/test_freeze.py b/tests/test_freeze.py index 8d31d20306..33e160fd7b 100644 --- a/tests/test_freeze.py +++ b/tests/test_freeze.py @@ -18,6 +18,7 @@ def on_mount(self): self.push_screen("myscreen") +@pytest.mark.anyio async def test_freeze(): """Regression test for https://github.com/Textualize/textual/issues/1608""" app = MyApp() diff --git a/tests/test_gc.py b/tests/test_gc.py index b21a7d9b72..7d37dfed0a 100644 --- a/tests/test_gc.py +++ b/tests/test_gc.py @@ -82,6 +82,7 @@ async def _count_app_nodes() -> None: # It looks like PyTest holds on to references to DOMNodes # So this will only pass if ran in isolation @pytest.mark.xfail +@pytest.mark.anyio async def test_gc(): """Regression test for https://github.com/Textualize/textual/issues/4959""" await _count_app_nodes() diff --git a/tests/test_header.py b/tests/test_header.py index 6ffdf0faad..3bc5665fee 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -3,6 +3,7 @@ from textual.widgets import Header +@pytest.mark.anyio async def test_screen_title_none_is_ignored(): class MyScreen(Screen): def compose(self): @@ -19,6 +20,7 @@ def on_mount(self): assert app.screen.query_one("HeaderTitle").text == "app title" +@pytest.mark.anyio async def test_screen_title_overrides_app_title(): class MyScreen(Screen): TITLE = "screen title" @@ -37,6 +39,7 @@ def on_mount(self): assert app.screen.query_one("HeaderTitle").text == "screen title" +@pytest.mark.anyio async def test_screen_title_reactive_updates_title(): class MyScreen(Screen): TITLE = "screen title" @@ -57,6 +60,7 @@ def on_mount(self): assert app.screen.query_one("HeaderTitle").text == "new screen title" +@pytest.mark.anyio async def test_app_title_reactive_does_not_update_title_when_screen_title_is_set(): class MyScreen(Screen): TITLE = "screen title" @@ -77,6 +81,7 @@ def on_mount(self): assert app.screen.query_one("HeaderTitle").text == "screen title" +@pytest.mark.anyio async def test_screen_sub_title_none_is_ignored(): class MyScreen(Screen): def compose(self): @@ -93,6 +98,7 @@ def on_mount(self): assert app.screen.query_one("HeaderTitle").sub_text == "app sub-title" +@pytest.mark.anyio async def test_screen_sub_title_overrides_app_sub_title(): class MyScreen(Screen): SUB_TITLE = "screen sub-title" @@ -111,6 +117,7 @@ def on_mount(self): assert app.screen.query_one("HeaderTitle").sub_text == "screen sub-title" +@pytest.mark.anyio async def test_screen_sub_title_reactive_updates_sub_title(): class MyScreen(Screen): SUB_TITLE = "screen sub-title" @@ -131,6 +138,7 @@ def on_mount(self): assert app.screen.query_one("HeaderTitle").sub_text == "new screen sub-title" +@pytest.mark.anyio async def test_app_sub_title_reactive_does_not_update_sub_title_when_screen_sub_title_is_set(): class MyScreen(Screen): SUB_TITLE = "screen sub-title" diff --git a/tests/test_issue_4248.py b/tests/test_issue_4248.py index beada864f9..518a76ef4e 100644 --- a/tests/test_issue_4248.py +++ b/tests/test_issue_4248.py @@ -4,6 +4,7 @@ from textual.widgets import Label +@pytest.mark.anyio async def test_issue_4248() -> None: """Various forms of click parameters should be fine.""" diff --git a/tests/test_keymap.py b/tests/test_keymap.py index d7615bd057..e87d20edd8 100644 --- a/tests/test_keymap.py +++ b/tests/test_keymap.py @@ -41,6 +41,7 @@ def handle_bindings_clash( self.clashed_node = node +@pytest.mark.anyio async def test_keymap_default_binding_replaces_old_binding(): app = Counter({"app.increment": "right,k"}) async with app.run_test() as pilot: @@ -53,6 +54,7 @@ async def test_keymap_default_binding_replaces_old_binding(): assert app.count == 2 +@pytest.mark.anyio async def test_keymap_sends_message_when_clash(): app = Counter({"app.increment": "d"}) async with app.run_test() as pilot: @@ -66,6 +68,7 @@ async def test_keymap_sends_message_when_clash(): assert clash.id == "app.increment" +@pytest.mark.anyio async def test_keymap_with_unknown_id_is_noop(): app = Counter({"this.is.an.unknown.id": "d"}) async with app.run_test() as pilot: @@ -73,6 +76,7 @@ async def test_keymap_with_unknown_id_is_noop(): assert app.count == -1 +@pytest.mark.anyio async def test_keymap_inherited_bindings_same_id(): """When a child widget inherits from a parent widget, if they have a binding with the same ID, then both parent and child bindings will @@ -134,6 +138,7 @@ def on_mount(self) -> None: assert child_counter == 1 +@pytest.mark.anyio async def test_keymap_child_with_different_id_overridden(): """Ensures that overriding a parent binding doesn't influence a child binding with a different ID.""" @@ -194,6 +199,7 @@ def on_mount(self) -> None: assert child_counter == 1 +@pytest.mark.anyio async def test_set_keymap_before_app_mount(): """Ensure we can set the keymap before mount without crash. diff --git a/tests/test_keys.py b/tests/test_keys.py index e9de532759..d2261d094b 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -24,6 +24,7 @@ def test_character_to_key(character: str, key: str) -> None: assert _character_to_key(character) == key +@pytest.mark.anyio async def test_character_bindings(): """Test you can bind to a character as well as a longer key name.""" counter = 0 diff --git a/tests/test_lazy.py b/tests/test_lazy.py index e862d0ce1f..f74c80b7aa 100644 --- a/tests/test_lazy.py +++ b/tests/test_lazy.py @@ -13,6 +13,7 @@ def compose(self) -> ComposeResult: yield Label(id="bar") +@pytest.mark.anyio async def test_lazy(): app = LazyApp() async with app.run_test() as pilot: @@ -34,6 +35,7 @@ def compose(self) -> ComposeResult: yield Label(id="baz") +@pytest.mark.anyio async def test_lazy_reveal(): app = RevealApp() async with app.run_test() as pilot: diff --git a/tests/test_links.py b/tests/test_links.py index 37cb286a4d..01535b14d6 100644 --- a/tests/test_links.py +++ b/tests/test_links.py @@ -3,6 +3,7 @@ from textual.widgets import Label +@pytest.mark.anyio async def test_links() -> None: """Regression test for https://github.com/Textualize/textual/issues/4536""" messages: list[str] = [] diff --git a/tests/test_loading.py b/tests/test_loading.py index 262feac102..a01a875f5b 100644 --- a/tests/test_loading.py +++ b/tests/test_loading.py @@ -20,6 +20,7 @@ def action_loading(self): self.query_one(VerticalScroll).loading = True +@pytest.mark.anyio async def test_loading_disables_and_remove_scrollbars(): app = LoadingApp() async with app.run_test() as pilot: diff --git a/tests/test_log.py b/tests/test_log.py index 4bbea14d42..02393f2708 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -9,6 +9,7 @@ def test_process_line(): assert log._process_line("\0foo") == "�foo" +@pytest.mark.anyio async def test_disabled_log_no_attribute_error() -> None: """Ensure that initializing the log with disabled=True does not raise an AttributeError. diff --git a/tests/test_markdown.py b/tests/test_markdown.py index 4d1214c0d8..79e218ebf2 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -78,6 +78,7 @@ def compose(self) -> ComposeResult: ), ], ) +@pytest.mark.anyio async def test_markdown_nodes( document: str, expected_nodes: list[Widget | list[Widget]] ) -> None: @@ -95,6 +96,7 @@ def markdown_nodes(root: Widget) -> Iterator[MarkdownBlock]: ] == expected_nodes +@pytest.mark.anyio async def test_softbreak_split_links_rendered_correctly() -> None: """Test for https://github.com/Textualize/textual/issues/2805""" @@ -119,6 +121,7 @@ async def test_softbreak_split_links_rendered_correctly() -> None: assert paragraph._text.spans == expected_spans +@pytest.mark.anyio async def test_load_non_existing_file() -> None: """Loading a file that doesn't exist should result in the obvious error.""" async with MarkdownApp("").run_test() as pilot: @@ -135,6 +138,7 @@ async def test_load_non_existing_file() -> None: ("hello-there", True), ], ) +@pytest.mark.anyio async def test_goto_anchor(anchor: str, found: bool) -> None: """Going to anchors should return a boolean: whether the anchor was found.""" document = "# Hello There\n\nGeneral.\n" @@ -143,6 +147,7 @@ async def test_goto_anchor(anchor: str, found: bool) -> None: assert markdown.goto_anchor(anchor) is found +@pytest.mark.anyio async def test_update_of_document_posts_table_of_content_update_message() -> None: """Updating the document should post a TableOfContentsUpdated message.""" @@ -166,6 +171,7 @@ def log_table_of_content_update( assert messages == ["TableOfContentsUpdated", "TableOfContentsUpdated"] +@pytest.mark.anyio async def test_link_in_markdown_table_posts_message_when_clicked(): """A link inside a markdown table should post a `Markdown.LinkClicked` message when clicked. @@ -199,6 +205,7 @@ def log_markdown_link_clicked( assert app.messages == ["LinkClicked"] +@pytest.mark.anyio async def test_markdown_quoting(): # https://github.com/Textualize/textual/issues/3350 links = [] diff --git a/tests/test_markdownviewer.py b/tests/test_markdownviewer.py index a1646f5a2e..424e42e55b 100644 --- a/tests/test_markdownviewer.py +++ b/tests/test_markdownviewer.py @@ -37,6 +37,7 @@ async def on_mount(self) -> None: @pytest.mark.parametrize("link", [0, 1]) +@pytest.mark.anyio async def test_markdown_file_viewer_anchor_link(tmp_path, link: int) -> None: """Test https://github.com/Textualize/textual/issues/3094""" async with MarkdownFileViewerApp(Path(tmp_path) / "test.md").run_test() as pilot: @@ -59,6 +60,7 @@ async def on_mount(self) -> None: @pytest.mark.parametrize("link", [0, 1]) +@pytest.mark.anyio async def test_markdown_string_viewer_anchor_link(link: int) -> None: """Test https://github.com/Textualize/textual/issues/3094 @@ -73,6 +75,7 @@ async def test_markdown_string_viewer_anchor_link(link: int) -> None: @pytest.mark.parametrize("text", ["Hey [[/test]]", "[i]Hey there[/i]"]) +@pytest.mark.anyio async def test_headings_that_look_like_they_contain_markup(text: str) -> None: """Regression test for https://github.com/Textualize/textual/issues/3689. diff --git a/tests/test_masked_input.py b/tests/test_masked_input.py index 72b4d9dc09..493cb72a69 100644 --- a/tests/test_masked_input.py +++ b/tests/test_masked_input.py @@ -28,6 +28,7 @@ def on_changed_or_submitted(self, event: InputEvent) -> None: self.messages.append(event) +@pytest.mark.anyio async def test_missing_required(): app = InputApp(">9999-99-99") async with app.run_test() as pilot: @@ -47,6 +48,7 @@ async def test_missing_required(): ) +@pytest.mark.anyio async def test_valid_required(): app = InputApp(">9999-99-99") async with app.run_test() as pilot: @@ -58,6 +60,7 @@ async def test_valid_required(): assert app.messages[0].validation_result == ValidationResult.success() +@pytest.mark.anyio async def test_missing_optional(): app = InputApp(">9999-99-00") async with app.run_test() as pilot: @@ -69,6 +72,7 @@ async def test_missing_optional(): assert app.messages[0].validation_result == ValidationResult.success() +@pytest.mark.anyio async def test_editing(): serial = "ABCDE-FGHIJ-KLMNO-PQRST" app = InputApp(">NNNNN-NNNNN-NNNNN-NNNNN;_") @@ -94,6 +98,7 @@ async def test_editing(): assert input.cursor_position == len(serial) +@pytest.mark.anyio async def test_key_movement_actions(): serial = "ABCDE-FGHIJ-KLMNO-PQRST" app = InputApp(">NNNNN-NNNNN-NNNNN-NNNNN;_") @@ -114,6 +119,7 @@ async def test_key_movement_actions(): assert input.cursor_position == 6 +@pytest.mark.anyio async def test_key_modification_actions(): serial = "ABCDE-FGHIJ-KLMNO-PQRST" app = InputApp(">NNNNN-NNNNN-NNNNN-NNNNN;_") @@ -152,6 +158,7 @@ async def test_key_modification_actions(): assert input.value == "" +@pytest.mark.anyio async def test_cursor_word_right_after_last_separator(): app = InputApp(">NNN-NNN-NNN-NNNNN;_") async with app.run_test(): @@ -162,6 +169,7 @@ async def test_cursor_word_right_after_last_separator(): assert input.cursor_position == 15 +@pytest.mark.anyio async def test_case_conversion_meta_characters(): app = InputApp("NN<-N!N>N") async with app.run_test() as pilot: @@ -171,6 +179,7 @@ async def test_case_conversion_meta_characters(): assert input.is_valid +@pytest.mark.anyio async def test_case_conversion_override(): app = InputApp(">- None: self.input_changed_events.append(event) +@pytest.mark.anyio async def test_message_queue_size(): """Test message queue size property.""" app = App() @@ -91,6 +95,7 @@ class TestMessage(Message): assert app.message_queue_size == 0 +@pytest.mark.anyio async def test_prevent() -> None: app = PreventTestApp() @@ -110,6 +115,7 @@ async def test_prevent() -> None: assert app.input_changed_events[0].value == "foo" +@pytest.mark.anyio async def test_prevent_with_call_next() -> None: """Test for https://github.com/Textualize/textual/issues/3166. @@ -146,6 +152,7 @@ def on_input_changed(self) -> None: assert hits == 2 +@pytest.mark.anyio async def test_prevent_default(): """Test that prevent_default doesn't apply when a message is bubbled.""" diff --git a/tests/test_modal.py b/tests/test_modal.py index 6a394e2bab..fe3ebdd7c4 100644 --- a/tests/test_modal.py +++ b/tests/test_modal.py @@ -83,6 +83,7 @@ def check_quit(quit: bool | None) -> None: self.push_screen(QuitScreen(), check_quit) +@pytest.mark.anyio async def test_modal_pop_screen(): # https://github.com/Textualize/textual/issues/4656 diff --git a/tests/test_mount.py b/tests/test_mount.py index 23567d2d06..bc64bed949 100644 --- a/tests/test_mount.py +++ b/tests/test_mount.py @@ -18,6 +18,7 @@ async def on_mount(self): self.renderable = "1234" +@pytest.mark.anyio async def test_render_only_after_mount(): """Regression test for https://github.com/Textualize/textual/issues/2914""" app = App() diff --git a/tests/test_on.py b/tests/test_on.py index 7483a7be9f..f605e0cdba 100644 --- a/tests/test_on.py +++ b/tests/test_on.py @@ -13,6 +13,7 @@ from textual.widgets import Button, TabbedContent, TabPane +@pytest.mark.anyio async def test_on_button_pressed() -> None: """Test handlers with @on decorator.""" @@ -55,6 +56,7 @@ def on_button_pressed(self): ] +@pytest.mark.anyio async def test_on_inheritance() -> None: """Test on decorator and inheritance.""" pressed: list[str] = [] @@ -122,6 +124,7 @@ def foo(): pass +@pytest.mark.anyio async def test_on_arbitrary_attributes() -> None: log: list[str] = [] @@ -169,6 +172,7 @@ def post_child(self) -> None: self.post_message(self.Child(self)) +@pytest.mark.anyio async def test_fire_on_inherited_message() -> None: """Handlers should fire when descendant messages are posted.""" @@ -196,6 +200,7 @@ def on_mount(self) -> None: assert posted == ["parent", "child", "parent"] +@pytest.mark.anyio async def test_fire_inherited_on_single_handler() -> None: """Test having parent/child messages on a single handler.""" @@ -220,6 +225,7 @@ def on_mount(self) -> None: assert posted == ["either Parent", "either Child"] +@pytest.mark.anyio async def test_fire_inherited_on_single_handler_multi_selector() -> None: """Test having parent/child messages on a single handler but with different selectors.""" @@ -274,6 +280,7 @@ def on_mount(self) -> None: ] +@pytest.mark.anyio async def test_fire_inherited_and_on_methods() -> None: posted: list[str] = [] @@ -328,6 +335,7 @@ def post_child(self) -> None: self.post_message(self.Child()) +@pytest.mark.anyio async def test_fire_on_inherited_message_plus_mixins() -> None: """Handlers should fire when descendant messages are posted, without mixins messing things up.""" @@ -355,6 +363,7 @@ def on_mount(self) -> None: assert posted == ["parent", "child", "parent"] +@pytest.mark.anyio async def test_on_with_enter_and_leave_events(): class EnterLeaveApp(App): messages = [] diff --git a/tests/test_overflow_change.py b/tests/test_overflow_change.py index 7f5d7eb46e..0d52cabd06 100644 --- a/tests/test_overflow_change.py +++ b/tests/test_overflow_change.py @@ -4,6 +4,7 @@ from textual.containers import VerticalScroll +@pytest.mark.anyio async def test_overflow_change_updates_virtual_size_appropriately(): class MyApp(App): def compose(self): diff --git a/tests/test_paste.py b/tests/test_paste.py index 6cfc3951fa..691d561069 100644 --- a/tests/test_paste.py +++ b/tests/test_paste.py @@ -3,6 +3,7 @@ from textual.widgets import Input +@pytest.mark.anyio async def test_paste_app(): paste_events = [] @@ -19,6 +20,7 @@ def on_paste(self, event): assert paste_events[0].text == "Hello" +@pytest.mark.anyio async def test_empty_paste(): """Regression test for https://github.com/Textualize/textual/issues/2563.""" diff --git a/tests/test_pilot.py b/tests/test_pilot.py index c212fccaae..7982cac8e9 100644 --- a/tests/test_pilot.py +++ b/tests/test_pilot.py @@ -47,6 +47,7 @@ def compose(self): yield Button() +@pytest.mark.anyio async def test_pilot_press_ascii_chars(): """Test that the pilot can press most ASCII characters as keys.""" keys_pressed = [] @@ -61,6 +62,7 @@ def on_key(self, event: events.Key) -> None: assert keys_pressed[-1] == char +@pytest.mark.anyio async def test_pilot_exception_catching_app_compose(): """Ensuring that test frameworks are aware of exceptions inside compose methods when running via Pilot run_test().""" @@ -75,6 +77,7 @@ def compose(self) -> ComposeResult: pass +@pytest.mark.anyio async def test_pilot_exception_catching_widget_compose(): class SomeScreen(Screen[None]): def compose(self) -> ComposeResult: @@ -90,6 +93,7 @@ def on_mount(self) -> None: pass +@pytest.mark.anyio async def test_pilot_exception_catching_action(): """Ensure that exceptions inside action handlers are presented to the test framework when running via Pilot run_test().""" @@ -105,6 +109,7 @@ def action_beep(self) -> None: await pilot.press("b") +@pytest.mark.anyio async def test_pilot_exception_catching_worker(): class SimpleAppThatCrashes(App[None]): def on_mount(self) -> None: @@ -120,6 +125,7 @@ async def crash(self) -> None: assert exc.type is ZeroDivisionError +@pytest.mark.anyio async def test_pilot_click_screen(): """Regression test for https://github.com/Textualize/textual/issues/3395. @@ -129,6 +135,7 @@ async def test_pilot_click_screen(): await pilot.click() +@pytest.mark.anyio async def test_pilot_hover_screen(): """Regression test for https://github.com/Textualize/textual/issues/3395. @@ -215,6 +222,7 @@ async def test_pilot_hover_screen(): ("mouse_up", (5, 5), (7, -1)), # Top-right of screen. ], ) +@pytest.mark.anyio async def test_pilot_target_outside_screen_errors(method, screen_size, offset): """Make sure that targeting a click/hover completely outside of the screen errors.""" app = App() @@ -268,6 +276,7 @@ async def test_pilot_target_outside_screen_errors(method, screen_size, offset): ("mouse_up", (40, 12)), # Right in the middle. ], ) +@pytest.mark.anyio async def test_pilot_target_inside_screen_is_fine_with_correct_coordinate_system( method, offset ): @@ -304,6 +313,7 @@ async def test_pilot_target_inside_screen_is_fine_with_correct_coordinate_system ("mouse_up", Button), ], ) +@pytest.mark.anyio async def test_pilot_target_on_widget_that_is_not_visible_errors(method, target): """Make sure that clicking a widget that is not scrolled into view raises an error.""" app = ManyLabelsApp() @@ -317,6 +327,7 @@ async def test_pilot_target_on_widget_that_is_not_visible_errors(method, target) @pytest.mark.parametrize("method", ["click", "hover", "mouse_down", "mouse_up"]) +@pytest.mark.anyio async def test_pilot_target_widget_under_another_widget(method): """The targeting method should return False when the targeted widget is covered.""" @@ -343,6 +354,7 @@ def on_mount(self): @pytest.mark.parametrize("method", ["click", "hover", "mouse_down", "mouse_up"]) +@pytest.mark.anyio async def test_pilot_target_visible_widget(method): """The targeting method should return True when the targeted widget is hit.""" @@ -381,6 +393,7 @@ def compose(self): ("mouse_up", (70, 0)), ], ) +@pytest.mark.anyio async def test_pilot_target_screen_always_true(method, offset): app = ManyLabelsApp() async with app.run_test(size=(80, 24)) as pilot: @@ -388,6 +401,7 @@ async def test_pilot_target_screen_always_true(method, offset): assert (await pilot_method(offset=offset)) is True +@pytest.mark.anyio async def test_pilot_resize_terminal(): app = App() async with app.run_test(size=(80, 24)) as pilot: @@ -400,6 +414,7 @@ async def test_pilot_resize_terminal(): assert app.screen.size == (27, 15) +@pytest.mark.anyio async def test_fail_early(): # https://github.com/Textualize/textual/issues/3282 class MyApp(App): @@ -411,6 +426,7 @@ class MyApp(App): await pilot.press("enter") +@pytest.mark.anyio async def test_click_by_widget(): """Test that click accept a Widget instance.""" pressed = False @@ -429,6 +445,7 @@ def on_button_pressed(self): @pytest.mark.parametrize("times", [1, 2, 3]) +@pytest.mark.anyio async def test_click_times(times: int): """Test that Pilot.click() can be called with a `times` argument.""" diff --git a/tests/test_progress_bar.py b/tests/test_progress_bar.py index 83bcb724bc..e2c91faf45 100644 --- a/tests/test_progress_bar.py +++ b/tests/test_progress_bar.py @@ -142,6 +142,7 @@ def test_go_back_to_indeterminate(): (False, False, False), ], ) +@pytest.mark.anyio async def test_show_sub_widgets(show_bar: bool, show_percentage: bool, show_eta: bool): class PBApp(App[None]): def compose(self): diff --git a/tests/test_query.py b/tests/test_query.py index 0c0759b653..8aa97e21d9 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -14,6 +14,7 @@ from textual.widgets import Input, Label +@pytest.mark.anyio async def test_query_errors(): app = App() async with app.run_test(): @@ -238,6 +239,7 @@ class App(Widget): app.query("#foo").exclude("#2") +@pytest.mark.anyio async def test_universal_selector_doesnt_select_self(): class ExampleApp(App): def compose(self) -> ComposeResult: @@ -261,6 +263,7 @@ def compose(self) -> ComposeResult: assert not any(node.id == "root-container" for node in results) +@pytest.mark.anyio async def test_query_set_styles_invalid_css_raises_error(): app = App() async with app.run_test(): @@ -268,6 +271,7 @@ async def test_query_set_styles_invalid_css_raises_error(): app.query(Widget).set_styles(css="random-rule: 1fr;") +@pytest.mark.anyio async def test_query_set_styles_kwds(): class LabelApp(App): def compose(self): @@ -281,6 +285,7 @@ def compose(self): assert app.query_one(Label).styles.color == Color(255, 0, 0) +@pytest.mark.anyio async def test_query_set_styles_css_and_kwds(): class LabelApp(App): def compose(self): @@ -298,6 +303,7 @@ def compose(self): assert app.query_one(Label).styles.background == Color(255, 0, 0) +@pytest.mark.anyio async def test_query_set_styles_css(): class LabelApp(App): def compose(self): @@ -318,6 +324,7 @@ def compose(self): @pytest.mark.parametrize( "args", [(False, False), (True, False), (True, True), (False, True)] ) +@pytest.mark.anyio async def test_query_refresh(args): refreshes = [] @@ -336,6 +343,7 @@ def compose(self): assert refreshes[-1] == args +@pytest.mark.anyio async def test_query_focus_blur(): class FocusApp(App): AUTO_FOCUS = None diff --git a/tests/test_reactive.py b/tests/test_reactive.py index 54b57e337e..2759097da9 100644 --- a/tests/test_reactive.py +++ b/tests/test_reactive.py @@ -14,6 +14,7 @@ NEW_VALUE = 1_000_000 +@pytest.mark.anyio async def test_watch(): """Test that changes to a watched reactive attribute happen immediately.""" @@ -37,6 +38,7 @@ def watch_count(self, value: int) -> None: assert app.watcher_call_count == 0 +@pytest.mark.anyio async def test_watch_async_init_false(): """Ensure that async watchers are called eventually when set by user code""" @@ -65,6 +67,7 @@ async def watch_count(self, old_value: int, new_value: int) -> None: assert app.watcher_new_value == NEW_VALUE # new_value passed to watch method +@pytest.mark.anyio async def test_watch_async_init_true(): """Ensure that when init is True in a reactive, its async watcher gets called by Textual eventually, even when the user does not set the value themselves.""" @@ -94,6 +97,7 @@ async def watch_count(self, old_value: int, new_value: int) -> None: assert app.watcher_new_value == OLD_VALUE # The value wasn't changed +@pytest.mark.anyio async def test_watch_init_false_always_update_false(): class WatcherInitFalse(App): count = reactive(0, init=False) @@ -112,6 +116,7 @@ def watch_count(self, new_value: int) -> None: assert app.watcher_call_count == 1 +@pytest.mark.anyio async def test_watch_init_true(): class WatcherInitTrue(App): count = var(OLD_VALUE) @@ -130,6 +135,7 @@ def watch_count(self, new_value: int) -> None: assert app.watcher_call_count == 2 # Watcher is NOT called again +@pytest.mark.anyio async def test_reactive_always_update(): calls = [] @@ -157,6 +163,7 @@ def watch_last_name(self, value): assert calls == ["first_name Darren", "first_name abc", "last_name def"] +@pytest.mark.anyio async def test_reactive_with_callable_default(): """A callable can be supplied as the default value for a reactive. Textual will call it in order to retrieve the default value.""" @@ -174,6 +181,7 @@ def watch_value(self, new_value): assert app.watcher_called_with == 123 +@pytest.mark.anyio async def test_validate_init_true(): """When init is True for a reactive attribute, Textual should call the validator AND the watch method when the app starts.""" @@ -194,6 +202,7 @@ def validate_count(self, value: int) -> int: assert validator_call_count == 1 +@pytest.mark.anyio async def test_validate_init_true_set_before_dom_ready(): """When init is True for a reactive attribute, Textual should call the validator AND the watch method when the app starts.""" @@ -214,6 +223,7 @@ def validate_count(self, value: int) -> int: assert validator_call_count == 1 +@pytest.mark.anyio async def test_reactive_compute_first_time_set(): class ReactiveComputeFirstTimeSet(App): number = reactive(1) @@ -227,6 +237,7 @@ def compute_double_number(self): assert app.double_number == 2 +@pytest.mark.anyio async def test_reactive_method_call_order(): class CallOrder(App): count = reactive(OLD_VALUE, init=False) @@ -262,6 +273,7 @@ def compute_count_times_ten(self) -> int: assert app.count_times_ten == (NEW_VALUE + 1) * 10 +@pytest.mark.anyio async def test_premature_reactive_call(): watcher_called = False @@ -286,6 +298,7 @@ def compose(self) -> ComposeResult: app.exit() +@pytest.mark.anyio async def test_reactive_inheritance(): """Check that inheritance works as expected for reactives.""" @@ -330,6 +343,7 @@ class Tertiary(Secondary): assert tertiary.baz == "baz" +@pytest.mark.anyio async def test_compute(): """Check compute method is called.""" @@ -360,6 +374,7 @@ def compute_count_double(self) -> int: app.count_double = 100 +@pytest.mark.anyio async def test_watch_compute(): """Check that watching a computed attribute works.""" @@ -393,6 +408,7 @@ def watch_show_ac(self, show_ac: bool) -> None: assert watch_called == [True, True, False, False, True, True, False, False] +@pytest.mark.anyio async def test_public_and_private_watch() -> None: """If a reactive/var has public and private watches both should get called.""" @@ -415,6 +431,7 @@ def _watch_counter(self) -> None: assert calls["public"] is True +@pytest.mark.anyio async def test_private_validate() -> None: calls: dict[str, bool] = {"private": False} @@ -430,6 +447,7 @@ def _validate_counter(self, _: int) -> None: assert calls["private"] is True +@pytest.mark.anyio async def test_public_and_private_validate() -> None: """If a reactive/var has public and private validate both should get called.""" @@ -452,6 +470,7 @@ def _validate_counter(self, _: int) -> None: assert calls["public"] is True +@pytest.mark.anyio async def test_public_and_private_validate_order() -> None: """The private validate should be called first.""" @@ -473,6 +492,7 @@ def _validate_value(self, value: int) -> int: assert pilot.app.value == 73 +@pytest.mark.anyio async def test_public_and_private_compute() -> None: """If a reactive/var has public and private compute both should get called.""" @@ -488,6 +508,7 @@ def _compute_counter(self): pass +@pytest.mark.anyio async def test_private_compute() -> None: class PrivateComputeTest(App): double = var(0, init=False) @@ -501,6 +522,7 @@ def _compute_double(self) -> int: assert pilot.app.double == 10 +@pytest.mark.anyio async def test_async_reactive_watch_callbacks_go_on_the_watcher(): """Regression test for https://github.com/Textualize/textual/issues/3036. @@ -540,6 +562,7 @@ async def callback(self): assert from_app +@pytest.mark.anyio async def test_sync_reactive_watch_callbacks_go_on_the_watcher(): """Regression test for https://github.com/Textualize/textual/issues/3036. @@ -579,6 +602,7 @@ def callback(self): assert from_app +@pytest.mark.anyio async def test_set_reactive(): """Test set_reactive doesn't call watchers.""" @@ -602,6 +626,7 @@ def compose(self) -> ComposeResult: assert app.query_one(MyWidget).foo == "foobar" +@pytest.mark.anyio async def test_no_duplicate_external_watchers() -> None: """Make sure we skip duplicated watchers.""" @@ -630,6 +655,7 @@ def callback(self) -> None: assert counter == 2 +@pytest.mark.anyio async def test_external_watch_init_does_not_propagate() -> None: """Regression test for https://github.com/Textualize/textual/issues/3878. @@ -668,6 +694,7 @@ def watch_test_2_extra() -> None: assert logs.count("test_2") == 1 +@pytest.mark.anyio async def test_external_watch_init_does_not_propagate_to_externals() -> None: """Regression test for https://github.com/Textualize/textual/issues/3878. @@ -709,6 +736,7 @@ def second_callback() -> None: assert logs == ["first", "second", "first", "second"] +@pytest.mark.anyio async def test_message_sender_from_reactive() -> None: """Test that the sender of a message comes from the reacting widget.""" @@ -745,6 +773,7 @@ def compose(self) -> ComposeResult: assert message_senders == [pilot.app.query_one(TestWidget)] +@pytest.mark.anyio async def test_mutate_reactive() -> None: """Test explicitly mutating reactives""" @@ -785,6 +814,7 @@ def compose(self) -> ComposeResult: assert watched_names == [[], ["Paul"], ["Jessica"]] +@pytest.mark.anyio async def test_mutate_reactive_data_bind() -> None: """https://github.com/Textualize/textual/issues/4825""" diff --git a/tests/test_rlock.py b/tests/test_rlock.py index 40cc4281b2..9c56f5c2ce 100644 --- a/tests/test_rlock.py +++ b/tests/test_rlock.py @@ -5,6 +5,7 @@ from textual.rlock import RLock +@pytest.mark.anyio async def test_simple_lock(): lock = RLock() # Starts not locked @@ -30,6 +31,7 @@ async def test_simple_lock(): lock.release() +@pytest.mark.anyio async def test_multiple_tasks() -> None: """Check RLock prevents other tasks from acquiring lock.""" lock = RLock() @@ -37,6 +39,7 @@ async def test_multiple_tasks() -> None: started: list[int] = [] done: list[int] = [] + @pytest.mark.anyio async def test_task(n: int) -> None: started.append(n) async with lock: diff --git a/tests/test_screen_modes.py b/tests/test_screen_modes.py index bf246a242b..5e8fe1e094 100644 --- a/tests/test_screen_modes.py +++ b/tests/test_screen_modes.py @@ -78,6 +78,7 @@ def on_mount(self): return ModesApp +@pytest.mark.anyio async def test_mode_setup(ModesApp: Type[App]): app = ModesApp() async with app.run_test(): @@ -85,6 +86,7 @@ async def test_mode_setup(ModesApp: Type[App]): assert str(app.screen.query_one(Label).renderable) == "one" +@pytest.mark.anyio async def test_switch_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test() as pilot: @@ -94,6 +96,7 @@ async def test_switch_mode(ModesApp: Type[App]): assert str(app.screen.query_one(Label).renderable) == "one" +@pytest.mark.anyio async def test_switch_same_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test() as pilot: @@ -103,6 +106,7 @@ async def test_switch_same_mode(ModesApp: Type[App]): assert str(app.screen.query_one(Label).renderable) == "one" +@pytest.mark.anyio async def test_switch_unknown_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test(): @@ -110,6 +114,7 @@ async def test_switch_unknown_mode(ModesApp: Type[App]): await app.switch_mode("unknown mode here") +@pytest.mark.anyio async def test_remove_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test() as pilot: @@ -120,6 +125,7 @@ async def test_remove_mode(ModesApp: Type[App]): assert "one" not in app._modes +@pytest.mark.anyio async def test_remove_active_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test(): @@ -127,6 +133,7 @@ async def test_remove_active_mode(ModesApp: Type[App]): app.remove_mode("one") +@pytest.mark.anyio async def test_add_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test() as pilot: @@ -136,6 +143,7 @@ async def test_add_mode(ModesApp: Type[App]): assert str(app.screen.query_one(Label).renderable) == "three" +@pytest.mark.anyio async def test_add_mode_duplicated(ModesApp: Type[App]): app = ModesApp() async with app.run_test(): @@ -143,6 +151,7 @@ async def test_add_mode_duplicated(ModesApp: Type[App]): app.add_mode("one", lambda: BaseScreen("one")) +@pytest.mark.anyio async def test_screen_stack_preserved(ModesApp: Type[App]): fruits = [] N = 5 @@ -168,6 +177,7 @@ async def test_screen_stack_preserved(ModesApp: Type[App]): await pilot.press("o") +@pytest.mark.anyio async def test_multiple_mode_callbacks(): written = [] diff --git a/tests/test_screens.py b/tests/test_screens.py index 36bcd4cb5d..4c851e8204 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -20,6 +20,7 @@ ) +@pytest.mark.anyio async def test_installed_screens(): class ScreensApp(App): SCREENS = { @@ -51,6 +52,7 @@ class ScreensApp(App): pilot.app.pop_screen() +@pytest.mark.anyio async def test_screens(): app = App() app._loop = asyncio.get_running_loop() @@ -142,6 +144,7 @@ async def test_screens(): await app._shutdown() +@pytest.mark.anyio async def test_auto_focus_on_screen_if_app_auto_focus_is_none(): """Setting app.AUTO_FOCUS = `None` means it is not taken into consideration.""" @@ -185,6 +188,7 @@ class MyApp(App[None]): assert app.focused.id == "two" +@pytest.mark.anyio async def test_auto_focus_on_screen_if_app_auto_focus_is_disabled(): """Setting app.AUTO_FOCUS = `None` means it is not taken into consideration.""" @@ -228,6 +232,7 @@ class MyApp(App[None]): assert app.focused.id == "two" +@pytest.mark.anyio async def test_auto_focus_inheritance(): """Setting app.AUTO_FOCUS = `None` means it is not taken into consideration.""" @@ -259,6 +264,7 @@ class MyApp(App[None]): app.pop_screen() +@pytest.mark.anyio async def test_auto_focus_skips_non_focusable_widgets(): class MyScreen(Screen[None]): def compose(self): @@ -275,6 +281,7 @@ def on_mount(self): assert isinstance(app.focused, Button) +@pytest.mark.anyio async def test_dismiss_non_top_screen(): class MyApp(App[None]): async def key_p(self) -> None: @@ -292,6 +299,7 @@ async def key_p(self) -> None: assert app.screen_stack == stack +@pytest.mark.anyio async def test_dismiss_action(): class ConfirmScreen(Screen[bool]): BINDINGS = [("y", "dismiss(True)", "Dismiss")] @@ -311,6 +319,7 @@ def callback(self, result: bool) -> None: assert app.bingo +@pytest.mark.anyio async def test_dismiss_action_no_argument(): class ConfirmScreen(Screen[bool]): BINDINGS = [("y", "dismiss", "Dismiss")] @@ -330,6 +339,7 @@ def callback(self, result: bool | None) -> None: assert app.bingo is None +@pytest.mark.anyio async def test_switch_screen_no_op(): """Regression test for https://github.com/Textualize/textual/issues/2650""" @@ -351,6 +361,7 @@ def on_mount(self): assert screen_id == id(app.screen) +@pytest.mark.anyio async def test_switch_screen_updates_results_callback_stack(): """Regression test for https://github.com/Textualize/textual/issues/2650""" @@ -382,6 +393,7 @@ def on_mount(self): assert app.screen._result_callbacks[-1].callback is None +@pytest.mark.anyio async def test_screen_receives_mouse_move_events(): class MouseMoveRecordingScreen(Screen): mouse_events = [] @@ -405,6 +417,7 @@ def on_mount(self): assert mouse_event.x, mouse_event.y == mouse_offset +@pytest.mark.anyio async def test_mouse_move_event_bubbles_to_screen_from_widget(): class MouseMoveRecordingScreen(Screen): mouse_events = [] @@ -441,6 +454,7 @@ def on_mount(self): ) +@pytest.mark.anyio async def test_push_screen_wait_for_dismiss() -> None: """Test push_screen returns result.""" @@ -477,6 +491,7 @@ async def action_exit(self) -> None: assert results == [False] +@pytest.mark.anyio async def test_push_screen_wait_for_dismiss_no_worker() -> None: """Test wait_for_dismiss raises NoActiveWorker when not using workers.""" @@ -505,6 +520,7 @@ async def action_exit(self) -> None: await pilot.press("x", "y") +@pytest.mark.anyio async def test_default_custom_screen() -> None: """Test we can override the default screen.""" @@ -522,6 +538,7 @@ def get_default_screen(self) -> Screen: assert app.screen is app.screen_stack[0] +@pytest.mark.anyio async def test_disallow_screen_instances() -> None: """Test that screen instances are disallowed.""" @@ -551,6 +568,7 @@ class Worst(App): MODES = {"OK": CustomScreen, 1: 2} # type: ignore +@pytest.mark.anyio async def test_worker_cancellation(): """Regression test for https://github.com/Textualize/textual/issues/4884 @@ -610,6 +628,7 @@ async def action_info(self) -> None: await pilot.press("enter") +@pytest.mark.anyio async def test_get_screen_with_expected_type(): """Test get_screen with expected type works""" diff --git a/tests/test_shutdown.py b/tests/test_shutdown.py index 4810aa3943..2e014e7783 100644 --- a/tests/test_shutdown.py +++ b/tests/test_shutdown.py @@ -9,6 +9,7 @@ def compose(self): yield Footer() +@pytest.mark.anyio async def test_shutdown(): # regression test for https://github.com/Textualize/textual/issues/4634 # Testing that an app with the footer doesn't deadlock diff --git a/tests/test_signal.py b/tests/test_signal.py index c8562c4ab4..92232cafec 100644 --- a/tests/test_signal.py +++ b/tests/test_signal.py @@ -5,6 +5,7 @@ from textual.widgets import Label +@pytest.mark.anyio async def test_signal(): """Test signal subscribe""" called = 0 @@ -75,6 +76,7 @@ def test_repr(): assert isinstance(repr(test_signal), str) +@pytest.mark.anyio async def test_signal_parameters(): str_result: str | None = None int_result: int | None = None diff --git a/tests/test_style_importance.py b/tests/test_style_importance.py index fde42d1bb5..ebbe2eb3d2 100644 --- a/tests/test_style_importance.py +++ b/tests/test_style_importance.py @@ -33,6 +33,7 @@ def compose(self) -> ComposeResult: yield Container(classes="more-specific") +@pytest.mark.anyio async def test_border_importance(): """Border without sides should support !important""" async with StyleApp().run_test() as pilot: @@ -44,6 +45,7 @@ async def test_border_importance(): assert border.right == desired +@pytest.mark.anyio async def test_outline_importance(): """Outline without sides should support !important""" async with StyleApp().run_test() as pilot: @@ -55,12 +57,14 @@ async def test_outline_importance(): assert outline.right == desired +@pytest.mark.anyio async def test_align_importance(): """Align without direction should support !important""" async with StyleApp().run_test() as pilot: assert pilot.app.query_one(Container).styles.align == ("right", "bottom") +@pytest.mark.anyio async def test_content_align_importance(): """Content align without direction should support !important""" async with StyleApp().run_test() as pilot: @@ -70,6 +74,7 @@ async def test_content_align_importance(): ) +@pytest.mark.anyio async def test_offset_importance(): """Offset without direction should support !important""" async with StyleApp().run_test() as pilot: @@ -78,6 +83,7 @@ async def test_offset_importance(): ) +@pytest.mark.anyio async def test_overflow_importance(): """Overflow without direction should support !important""" async with StyleApp().run_test() as pilot: @@ -85,6 +91,7 @@ async def test_overflow_importance(): assert pilot.app.query_one(Container).styles.overflow_y == "hidden" +@pytest.mark.anyio async def test_padding_importance(): """Padding without sides should support !important""" async with StyleApp().run_test() as pilot: @@ -95,6 +102,7 @@ async def test_padding_importance(): assert padding.right == 20 +@pytest.mark.anyio async def test_scrollbar_size_importance(): """Scrollbar size without direction should support !important""" async with StyleApp().run_test() as pilot: diff --git a/tests/test_style_inheritance.py b/tests/test_style_inheritance.py index 72b631a94a..35c2fbc920 100644 --- a/tests/test_style_inheritance.py +++ b/tests/test_style_inheritance.py @@ -2,6 +2,7 @@ from textual.widgets import Button, Static +@pytest.mark.anyio async def test_text_style_inheritance(): """Check that changes to text style are inherited in children.""" diff --git a/tests/test_suspend.py b/tests/test_suspend.py index b2d95cff0d..422b8d0d26 100644 --- a/tests/test_suspend.py +++ b/tests/test_suspend.py @@ -6,6 +6,7 @@ from textual.drivers.headless_driver import HeadlessDriver +@pytest.mark.anyio async def test_suspend_not_supported() -> None: """Suspending when not supported should raise an error.""" async with App().run_test() as pilot: @@ -16,6 +17,7 @@ async def test_suspend_not_supported() -> None: pass +@pytest.mark.anyio async def test_suspend_supported(capfd: pytest.CaptureFixture[str]) -> None: """Suspending when supported should call the relevant driver methods.""" diff --git a/tests/test_switch.py b/tests/test_switch.py index e0868118f1..1a9b33e4e2 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -2,6 +2,7 @@ from textual.widgets import Switch +@pytest.mark.anyio async def test_switch_click_doesnt_bubble_up(): """Regression test for https://github.com/Textualize/textual/issues/2366""" diff --git a/tests/test_tabbed_content.py b/tests/test_tabbed_content.py index 57a8874ebf..b865165a4b 100644 --- a/tests/test_tabbed_content.py +++ b/tests/test_tabbed_content.py @@ -8,6 +8,7 @@ from textual.widgets._tabbed_content import ContentTab +@pytest.mark.anyio async def test_tabbed_content_switch_via_ui(): """Check tab navigation via the user interface.""" @@ -67,6 +68,7 @@ def compose(self) -> ComposeResult: assert app.query_one("#baz-label").region +@pytest.mark.anyio async def test_tabbed_content_switch_via_code(): """Check tab navigation via code.""" @@ -109,6 +111,7 @@ def compose(self) -> ComposeResult: tabbed_content.active = "X" +@pytest.mark.anyio async def test_unsetting_tabbed_content_active(): """Check that setting `TabbedContent.active = ""` unsets active tab.""" @@ -137,6 +140,7 @@ def on_tabbed_content_cleared(self, event: TabbedContent.Cleared) -> None: assert isinstance(messages[0], TabbedContent.Cleared) +@pytest.mark.anyio async def test_tabbed_content_initial(): """Checked tabbed content with non-default tab.""" @@ -161,6 +165,7 @@ def compose(self) -> ComposeResult: assert not app.query_one("#baz-label").region +@pytest.mark.anyio async def test_tabbed_content_messages(): class TabbedApp(App): activation_history: list[Tab] = [] @@ -192,6 +197,7 @@ def on_tabbed_content_tab_activated( ] +@pytest.mark.anyio async def test_tabbed_content_add_later_from_empty(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -209,6 +215,7 @@ def compose(self) -> ComposeResult: assert tabbed_content.active == "test-1" +@pytest.mark.anyio async def test_tabbed_content_add_later_from_composed(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -229,6 +236,7 @@ def compose(self) -> ComposeResult: assert tabbed_content.active == "initial-1" +@pytest.mark.anyio async def test_tabbed_content_add_before_id(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -250,6 +258,7 @@ def compose(self) -> ComposeResult: ] +@pytest.mark.anyio async def test_tabbed_content_add_before_pane(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -275,6 +284,7 @@ def compose(self) -> ComposeResult: ] +@pytest.mark.anyio async def test_tabbed_content_add_before_badly(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -291,6 +301,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_tabbed_content_add_after(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -313,6 +324,7 @@ def compose(self) -> ComposeResult: ] +@pytest.mark.anyio async def test_tabbed_content_add_after_pane(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -339,6 +351,7 @@ def compose(self) -> ComposeResult: ] +@pytest.mark.anyio async def test_tabbed_content_add_after_badly(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -355,6 +368,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_tabbed_content_add_before_and_after(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -371,6 +385,7 @@ def compose(self) -> ComposeResult: ) +@pytest.mark.anyio async def test_tabbed_content_removal(): class TabbedApp(App[None]): cleared: var[int] = var(0) @@ -406,6 +421,7 @@ def on_tabbed_content_cleared(self) -> None: assert tabbed_content.active == "" +@pytest.mark.anyio async def test_tabbed_content_reversed_removal(): class TabbedApp(App[None]): cleared: var[int] = var(0) @@ -441,6 +457,7 @@ def on_tabbed_content_cleared(self) -> None: assert tabbed_content.active == "" +@pytest.mark.anyio async def test_tabbed_content_clear(): class TabbedApp(App[None]): cleared: var[int] = var(0) @@ -466,6 +483,7 @@ def on_tabbed_content_cleared(self) -> None: assert pilot.app.cleared == 1 +@pytest.mark.anyio async def test_disabling_does_not_deactivate_tab(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -480,6 +498,7 @@ def on_mount(self) -> None: assert app.query_one(TabbedContent).active == "tab-1" +@pytest.mark.anyio async def test_disabled_tab_cannot_be_clicked(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -496,6 +515,7 @@ def on_mount(self) -> None: assert app.query_one(TabbedContent).active == "tab-1" +@pytest.mark.anyio async def test_disabling_via_tabbed_content(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -512,6 +532,7 @@ def on_mount(self) -> None: assert app.query_one(TabbedContent).active == "tab-1" +@pytest.mark.anyio async def test_disabling_via_tab_pane(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -529,6 +550,7 @@ def on_mount(self) -> None: assert app.query_one(TabbedContent).active == "tab-1" +@pytest.mark.anyio async def test_creating_disabled_tab(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -544,6 +566,7 @@ def compose(self) -> ComposeResult: assert app.query_one(TabbedContent).active == "tab-1" +@pytest.mark.anyio async def test_navigation_around_disabled_tabs(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -576,6 +599,7 @@ def on_mount(self) -> None: assert tabbed_conent.active == "tab-4" +@pytest.mark.anyio async def test_reenabling_tab(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -598,6 +622,7 @@ def reenable(self) -> None: assert app.query_one(TabbedContent).active == "tab-2" +@pytest.mark.anyio async def test_reenabling_via_tabbed_content(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -620,6 +645,7 @@ def reenable(self) -> None: assert app.query_one(TabbedContent).active == "tab-2" +@pytest.mark.anyio async def test_reenabling_via_tab_pane(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -643,6 +669,7 @@ def reenable(self) -> None: assert app.query_one(TabbedContent).active == "tab-2" +@pytest.mark.anyio async def test_disabling_unknown_tab(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -655,6 +682,7 @@ def compose(self) -> ComposeResult: app.query_one(TabbedContent).disable_tab("foo") +@pytest.mark.anyio async def test_enabling_unknown_tab(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -667,6 +695,7 @@ def compose(self) -> ComposeResult: app.query_one(TabbedContent).enable_tab("foo") +@pytest.mark.anyio async def test_hide_unknown_tab(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -679,6 +708,7 @@ def compose(self) -> ComposeResult: app.query_one(TabbedContent).hide_tab("foo") +@pytest.mark.anyio async def test_show_unknown_tab(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -691,6 +721,7 @@ def compose(self) -> ComposeResult: app.query_one(TabbedContent).show_tab("foo") +@pytest.mark.anyio async def test_hide_show_messages(): hide_msg = False show_msg = False @@ -718,6 +749,7 @@ def on_tabs_tab_shown(self) -> None: assert show_msg +@pytest.mark.anyio async def test_hide_last_tab_means_no_tab_active(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -732,6 +764,7 @@ def compose(self) -> ComposeResult: assert not tabbed_content.active +@pytest.mark.anyio async def test_hiding_tabs_moves_active_to_next_tab(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -751,6 +784,7 @@ def compose(self) -> ComposeResult: assert tabbed_content.active == "tab-3" +@pytest.mark.anyio async def test_showing_tabs_does_not_change_active_tab(): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -774,6 +808,7 @@ def compose(self) -> ComposeResult: @pytest.mark.parametrize("tab_id", ["tab-1", "tab-2"]) +@pytest.mark.anyio async def test_showing_first_tab_activates_tab(tab_id: str): class TabbedApp(App[None]): def compose(self) -> ComposeResult: @@ -795,6 +830,7 @@ def compose(self) -> ComposeResult: assert tabbed_content.active == tab_id +@pytest.mark.anyio async def test_disabling_nested_tabs(): """Regression test for https://github.com/Textualize/textual/issues/3145.""" @@ -821,6 +857,7 @@ def compose(self) -> ComposeResult: await pilot.pause() +@pytest.mark.anyio async def test_hiding_nested_tabs(): """Regression test for https://github.com/Textualize/textual/issues/3145.""" @@ -847,6 +884,7 @@ def compose(self) -> ComposeResult: await pilot.pause() +@pytest.mark.anyio async def test_tabs_nested_in_tabbed_content_doesnt_crash(): """Regression test for https://github.com/Textualize/textual/issues/3412""" @@ -861,6 +899,7 @@ def compose(self) -> ComposeResult: await pilot.pause() +@pytest.mark.anyio async def test_tabs_nested_doesnt_interfere_with_ancestor_tabbed_content(): """When a Tabs is nested as a descendant in the DOM of a TabbedContent, the messages posted from that Tabs should not interfere with the TabbedContent. @@ -893,6 +932,7 @@ def compose(self) -> ComposeResult: assert tabbed_content.active == "outer1" +@pytest.mark.anyio async def test_disabling_tab_within_tabbed_content_stays_isolated(): """Disabling a tab within a tab pane should not affect the TabbedContent.""" diff --git a/tests/test_tabs.py b/tests/test_tabs.py index c43cc6bd68..c76516808a 100644 --- a/tests/test_tabs.py +++ b/tests/test_tabs.py @@ -8,11 +8,13 @@ from textual.widgets._tabs import Underline +@pytest.mark.anyio async def test_tab_label(): """It should be possible to access a tab's label.""" assert Tab("Pilot").label_text == "Pilot" +@pytest.mark.anyio async def test_tab_relabel(): """It should be possible to relabel a tab.""" tab = Tab("Pilot") @@ -21,6 +23,7 @@ async def test_tab_relabel(): assert tab.label_text == "Aeryn" +@pytest.mark.anyio async def test_compose_empty_tabs(): """It should be possible to create an empty Tabs.""" @@ -33,6 +36,7 @@ def compose(self) -> ComposeResult: assert pilot.app.query_one(Tabs).active_tab is None +@pytest.mark.anyio async def test_compose_tabs_from_strings(): """It should be possible to create a Tabs from some strings.""" @@ -47,6 +51,7 @@ def compose(self) -> ComposeResult: assert tabs.active_tab.id == "tab-1" +@pytest.mark.anyio async def test_compose_tabs_from_tabs(): """It should be possible to create a Tabs from some Tabs.""" @@ -66,6 +71,7 @@ def compose(self) -> ComposeResult: assert tabs.active_tab.id == "tab-1" +@pytest.mark.anyio async def test_add_tabs_later(): """It should be possible to add tabs later on in the app's cycle.""" @@ -87,6 +93,7 @@ def compose(self) -> ComposeResult: assert tabs.active_tab.id == "tab-1" +@pytest.mark.anyio async def test_add_tab_before(): """It should be possible to add a tab before another tab.""" @@ -112,6 +119,7 @@ def compose(self) -> ComposeResult: assert tabs.active == "tab-1" +@pytest.mark.anyio async def test_add_tab_before_badly(): """Test exceptions from badly adding a tab before another.""" @@ -139,6 +147,7 @@ def compose(self) -> ComposeResult: assert tabs.active == "tab-1" +@pytest.mark.anyio async def test_add_tab_after(): """It should be possible to add a tab after another tab.""" @@ -164,6 +173,7 @@ def compose(self) -> ComposeResult: assert tabs.active == "tab-1" +@pytest.mark.anyio async def test_add_tab_after_badly(): """Test exceptions from badly adding a tab after another.""" @@ -191,6 +201,7 @@ def compose(self) -> ComposeResult: assert tabs.active == "tab-1" +@pytest.mark.anyio async def test_add_tab_before_and_after(): """Attempting to add a tab before and after another is an error.""" @@ -208,6 +219,7 @@ def compose(self) -> ComposeResult: tabs.add_tab("John", before="tab-1", after="tab-1") +@pytest.mark.anyio async def test_remove_tabs(): """It should be possible to remove tabs.""" @@ -247,6 +259,7 @@ def compose(self) -> ComposeResult: assert tabs.active_tab is None +@pytest.mark.anyio async def test_remove_tabs_reversed(): """It should be possible to remove tabs.""" @@ -280,6 +293,7 @@ def compose(self) -> ComposeResult: assert tabs.active_tab is None +@pytest.mark.anyio async def test_clear_tabs(): """It should be possible to clear all tabs.""" @@ -297,6 +311,7 @@ def compose(self) -> ComposeResult: assert tabs.active_tab is None +@pytest.mark.anyio async def test_change_active_from_code(): """It should be possible to change the active tab from code..""" @@ -320,6 +335,7 @@ def compose(self) -> ComposeResult: assert tabs.active_tab is None +@pytest.mark.anyio async def test_navigate_tabs_with_keyboard(): """It should be possible to navigate tabs with the keyboard.""" @@ -350,6 +366,7 @@ def compose(self) -> ComposeResult: assert tabs.active == tabs.active_tab.id +@pytest.mark.anyio async def test_navigate_empty_tabs_with_keyboard(): """It should be possible to navigate an empty tabs with the keyboard.""" @@ -372,6 +389,7 @@ def compose(self) -> ComposeResult: assert tabs.active == "" +@pytest.mark.anyio async def test_navigate_tabs_with_mouse(): """It should be possible to navigate tabs with the mouse.""" @@ -417,12 +435,14 @@ def check_control(self, event: Tabs.TabActivated) -> None: assert event.control is event.tabs +@pytest.mark.anyio async def test_startup_messages(): """On startup there should be a tab activated message.""" async with TabsMessageCatchApp().run_test() as pilot: assert pilot.app.intended_handlers == ["on_tabs_tab_activated"] +@pytest.mark.anyio async def test_change_tab_with_code_messages(): """Changing tab in code should result in an activated tab message.""" async with TabsMessageCatchApp().run_test() as pilot: @@ -434,6 +454,7 @@ async def test_change_tab_with_code_messages(): ] +@pytest.mark.anyio async def test_remove_tabs_messages(): """Removing tabs should result in various messages.""" async with TabsMessageCatchApp().run_test() as pilot: @@ -451,6 +472,7 @@ async def test_remove_tabs_messages(): ] +@pytest.mark.anyio async def test_reverse_remove_tabs_messages(): """Removing tabs should result in various messages.""" async with TabsMessageCatchApp().run_test() as pilot: @@ -465,6 +487,7 @@ async def test_reverse_remove_tabs_messages(): ] +@pytest.mark.anyio async def test_keyboard_navigation_messages(): """Keyboard navigation should result in the expected messages.""" async with TabsMessageCatchApp().run_test() as pilot: @@ -479,6 +502,7 @@ async def test_keyboard_navigation_messages(): ] +@pytest.mark.anyio async def test_mouse_navigation_messages(): """Mouse navigation should result in the expected messages.""" async with TabsMessageCatchApp().run_test() as pilot: @@ -493,6 +517,7 @@ async def test_mouse_navigation_messages(): ] +@pytest.mark.anyio async def test_disabled_tab_is_not_activated_by_clicking_underline(): """Regression test for https://github.com/Textualize/textual/issues/4701""" diff --git a/tests/test_test_runner.py b/tests/test_test_runner.py index 8bb80d2071..81f31a69f2 100644 --- a/tests/test_test_runner.py +++ b/tests/test_test_runner.py @@ -2,6 +2,7 @@ from textual.app import App +@pytest.mark.anyio async def test_run_test() -> None: """Test the run_test context manager.""" keys_pressed: list[str] = [] diff --git a/tests/test_tooltips.py b/tests/test_tooltips.py index 5e78a2f30f..1ac448607b 100644 --- a/tests/test_tooltips.py +++ b/tests/test_tooltips.py @@ -33,6 +33,7 @@ def compose(self) -> ComposeResult: """ +@pytest.mark.anyio async def test_no_tip_gets_no_tooltip() -> None: """If there is no tooltip, none should show.""" async with TooltipApp().run_test(tooltips=True) as pilot: @@ -43,6 +44,7 @@ async def test_no_tip_gets_no_tooltip() -> None: assert pilot.app.query_one("#textual-tooltip").display is False +@pytest.mark.anyio async def test_tip_gets_a_tooltip() -> None: """If there is a tooltip, it should show.""" async with TooltipApp().run_test(tooltips=True) as pilot: @@ -53,6 +55,7 @@ async def test_tip_gets_a_tooltip() -> None: assert pilot.app.query_one("#textual-tooltip").display is True +@pytest.mark.anyio async def test_mouse_move_removes_a_tooltip() -> None: """If there is a mouse move when there is a tooltip, it should disappear.""" async with TooltipApp().run_test(tooltips=True) as pilot: @@ -66,6 +69,7 @@ async def test_mouse_move_removes_a_tooltip() -> None: assert pilot.app.query_one("#textual-tooltip").display is False +@pytest.mark.anyio async def test_removing_tipper_should_remove_tooltip() -> None: """If the tipping widget is removed, it should remove the tooltip.""" async with TooltipApp().run_test(tooltips=True) as pilot: @@ -79,6 +83,7 @@ async def test_removing_tipper_should_remove_tooltip() -> None: assert pilot.app.query_one("#textual-tooltip").display is False +@pytest.mark.anyio async def test_making_tipper_invisible_should_remove_tooltip() -> None: """If the tipping widget is made invisible, it should remove the tooltip.""" async with TooltipApp().run_test(tooltips=True) as pilot: @@ -92,6 +97,7 @@ async def test_making_tipper_invisible_should_remove_tooltip() -> None: assert pilot.app.query_one("#textual-tooltip").display is False +@pytest.mark.anyio async def test_making_tipper_not_displayed_should_remove_tooltip() -> None: """If the tipping widget is made display none, it should remove the tooltip.""" async with TooltipApp().run_test(tooltips=True) as pilot: @@ -105,6 +111,7 @@ async def test_making_tipper_not_displayed_should_remove_tooltip() -> None: assert pilot.app.query_one("#textual-tooltip").display is False +@pytest.mark.anyio async def test_making_tipper_shuffle_away_should_remove_tooltip() -> None: """If the tipping widget moves from under the cursor, it should remove the tooltip.""" async with TooltipApp().run_test(tooltips=True) as pilot: diff --git a/tests/test_unmount.py b/tests/test_unmount.py index 324a3812a3..72b8943bf0 100644 --- a/tests/test_unmount.py +++ b/tests/test_unmount.py @@ -6,6 +6,7 @@ from textual.screen import Screen +@pytest.mark.anyio async def test_unmount() -> None: """Test unmount events are received in reverse DOM order.""" unmount_ids: list[str] = [] diff --git a/tests/test_visible.py b/tests/test_visible.py index 3d991d8588..f5e8f78a23 100644 --- a/tests/test_visible.py +++ b/tests/test_visible.py @@ -3,6 +3,7 @@ from textual.widget import Widget +@pytest.mark.anyio async def test_visibility_changes() -> None: """Test changing visibility via code and CSS. @@ -44,6 +45,7 @@ def compose(self) -> ComposeResult: assert pilot.app.query_one("#hide-via-css").visible is False +@pytest.mark.anyio async def test_visible_is_inherited() -> None: """Regression test for https://github.com/Textualize/textual/issues/3071""" diff --git a/tests/test_widget.py b/tests/test_widget.py index 4f2918d1ce..e608e41935 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -28,6 +28,7 @@ ) +@pytest.mark.anyio async def test_widget_construct(): """Regression test for https://github.com/Textualize/textual/issues/5042""" @@ -136,6 +137,7 @@ async def hierarchy_app(): yield app +@pytest.mark.anyio async def test_get_child_by_id_gets_first_child(hierarchy_app): async with hierarchy_app.run_test(): parent = hierarchy_app.parent @@ -145,6 +147,7 @@ async def test_get_child_by_id_gets_first_child(hierarchy_app): assert parent.get_child_by_id(id="child2").id == "child2" +@pytest.mark.anyio async def test_get_child_by_id_no_matching_child(hierarchy_app): async with hierarchy_app.run_test() as pilot: parent = pilot.app.parent @@ -152,6 +155,7 @@ async def test_get_child_by_id_no_matching_child(hierarchy_app): parent.get_child_by_id(id="doesnt-exist") +@pytest.mark.anyio async def test_get_child_by_id_only_immediate_descendents(hierarchy_app): async with hierarchy_app.run_test() as pilot: parent = pilot.app.parent @@ -159,6 +163,7 @@ async def test_get_child_by_id_only_immediate_descendents(hierarchy_app): parent.get_child_by_id(id="grandchild1") +@pytest.mark.anyio async def test_get_child_by_type(): class GetChildApp(App): def compose(self) -> ComposeResult: @@ -177,6 +182,7 @@ def compose(self) -> ComposeResult: app.get_child_by_type(Label) +@pytest.mark.anyio async def test_get_widget_by_id_no_matching_child(hierarchy_app): async with hierarchy_app.run_test() as pilot: parent = pilot.app.parent @@ -184,6 +190,7 @@ async def test_get_widget_by_id_no_matching_child(hierarchy_app): parent.get_widget_by_id(id="i-dont-exist") +@pytest.mark.anyio async def test_get_widget_by_id_non_immediate_descendants(hierarchy_app): async with hierarchy_app.run_test() as pilot: parent = pilot.app.parent @@ -191,6 +198,7 @@ async def test_get_widget_by_id_non_immediate_descendants(hierarchy_app): assert result.id == "grandchild1" +@pytest.mark.anyio async def test_get_widget_by_id_immediate_descendants(hierarchy_app): async with hierarchy_app.run_test() as pilot: parent = pilot.app.parent @@ -198,6 +206,7 @@ async def test_get_widget_by_id_immediate_descendants(hierarchy_app): assert result.id == "child1" +@pytest.mark.anyio async def test_get_widget_by_id_doesnt_return_self(hierarchy_app): async with hierarchy_app.run_test() as pilot: parent = pilot.app.parent @@ -205,6 +214,7 @@ async def test_get_widget_by_id_doesnt_return_self(hierarchy_app): parent.get_widget_by_id("parent") +@pytest.mark.anyio async def test_get_widgets_app_delegated(hierarchy_app): # Check that get_child_by_id finds the parent, which is a child of the default Screen async with hierarchy_app.run_test() as pilot: @@ -217,6 +227,7 @@ async def test_get_widgets_app_delegated(hierarchy_app): assert grandchild.id == "grandchild1" +@pytest.mark.anyio async def test_widget_mount_ids_must_be_unique_mounting_all_in_one_go(hierarchy_app): async with hierarchy_app.run_test() as pilot: parent = pilot.app.parent @@ -227,6 +238,7 @@ async def test_widget_mount_ids_must_be_unique_mounting_all_in_one_go(hierarchy_ parent.mount(widget1, widget2) +@pytest.mark.anyio async def test_widget_mount_ids_must_be_unique_mounting_multiple_calls(hierarchy_app): async with hierarchy_app.run_test() as pilot: parent = pilot.app.parent @@ -273,6 +285,7 @@ def test_get_pseudo_class_state_focus(): # Regression test for https://github.com/Textualize/textual/issues/1634 +@pytest.mark.anyio async def test_remove(): class RemoveMeLabel(Label): async def on_mount(self) -> None: @@ -298,6 +311,7 @@ async def action_remove_all(self) -> None: # Regression test for https://github.com/Textualize/textual/issues/2079 +@pytest.mark.anyio async def test_remove_unmounted(): mounted = False @@ -323,6 +337,7 @@ def test_render_str() -> None: assert widget.render_str(content) is content +@pytest.mark.anyio async def test_compose_order() -> None: from textual.containers import Horizontal from textual.screen import Screen @@ -379,6 +394,7 @@ def test__allow_scroll_default(): assert not Widget()._allow_scroll +@pytest.mark.anyio async def test__allow_scroll(): from textual.containers import ScrollableContainer @@ -394,6 +410,7 @@ def compose(self): assert app.query_one(ScrollableContainer)._allow_scroll +@pytest.mark.anyio async def test_offset_getter_setter(): class OffsetApp(App): def compose(self): @@ -414,6 +431,7 @@ def test_get_set_tooltip(): assert widget.tooltip == "This is a tooltip." +@pytest.mark.anyio async def test_loading(): """Test setting the loading reactive.""" @@ -444,6 +462,7 @@ def compose(self) -> ComposeResult: assert label._cover_widget is None +@pytest.mark.anyio async def test_loading_button(): """Test loading indicator renders buttons unclickable.""" @@ -483,6 +502,7 @@ def action_inc(self) -> None: assert counter == 2 +@pytest.mark.anyio async def test_is_mounted_property(): class TestWidgetIsMountedApp(App): pass @@ -494,6 +514,7 @@ class TestWidgetIsMountedApp(App): assert widget.is_mounted is True +@pytest.mark.anyio async def test_mount_error_not_widget(): class NotWidgetApp(App): def compose(self) -> ComposeResult: @@ -505,6 +526,7 @@ def compose(self) -> ComposeResult: pass +@pytest.mark.anyio async def test_mount_error_bad_widget(): class DaftWidget(Widget): def __init__(self): @@ -521,6 +543,7 @@ def compose(self) -> ComposeResult: pass +@pytest.mark.anyio async def test_render_returns_text(): """Test that render processes console markup when returning a string.""" @@ -535,6 +558,7 @@ def render(self) -> str: assert render_result.plain == "Hello World!" +@pytest.mark.anyio async def test_sort_children() -> None: """Test the sort_children method.""" @@ -571,6 +595,7 @@ def compose(self) -> ComposeResult: ] +@pytest.mark.anyio async def test_sort_children_no_key() -> None: """Test sorting with no key.""" @@ -634,6 +659,7 @@ def test_lazy_loading() -> None: assert hasattr(widgets, "Label") +@pytest.mark.anyio async def test_of_type() -> None: class MyApp(App): def compose(self) -> ComposeResult: @@ -669,6 +695,7 @@ def compose(self) -> ComposeResult: assert not labels[4].is_even +@pytest.mark.anyio async def test_click_line_api_border(): """Regression test for https://github.com/Textualize/textual/issues/5634""" diff --git a/tests/test_widget_child_moving.py b/tests/test_widget_child_moving.py index f4c89b607f..afebbf2e25 100644 --- a/tests/test_widget_child_moving.py +++ b/tests/test_widget_child_moving.py @@ -6,6 +6,7 @@ from textual.widget import Widget, WidgetError +@pytest.mark.anyio async def test_move_child_no_direction() -> None: """Test moving a widget in a child list.""" async with App().run_test() as pilot: @@ -15,6 +16,7 @@ async def test_move_child_no_direction() -> None: pilot.app.screen.move_child(child) +@pytest.mark.anyio async def test_move_child_both_directions() -> None: """Test calling move_child with more than one direction.""" async with App().run_test() as pilot: @@ -24,6 +26,7 @@ async def test_move_child_both_directions() -> None: pilot.app.screen.move_child(child, before=1, after=2) +@pytest.mark.anyio async def test_move_child_not_our_child() -> None: """Test attempting to move a child that isn't ours.""" async with App().run_test() as pilot: @@ -33,6 +36,7 @@ async def test_move_child_not_our_child() -> None: pilot.app.screen.move_child(Widget(), before=child) +@pytest.mark.anyio async def test_move_child_to_outside() -> None: """Test attempting to move relative to a widget that isn't a child.""" async with App().run_test() as pilot: @@ -49,6 +53,7 @@ async def test_move_child_to_outside() -> None: "after", ], ) +@pytest.mark.anyio async def test_move_child_index_in_relation_to_itself_index(reference: str) -> None: """Regression test for https://github.com/Textualize/textual/issues/1743""" @@ -67,6 +72,7 @@ async def test_move_child_index_in_relation_to_itself_index(reference: str) -> N "after", ], ) +@pytest.mark.anyio async def test_move_child_index_in_relation_to_itself_widget(reference: str) -> None: """Regression test for https://github.com/Textualize/textual/issues/1743""" @@ -85,6 +91,7 @@ async def test_move_child_index_in_relation_to_itself_widget(reference: str) -> "after", ], ) +@pytest.mark.anyio async def test_move_child_widget_in_relation_to_itself_index(reference: str) -> None: """Regression test for https://github.com/Textualize/textual/issues/1743""" @@ -103,6 +110,7 @@ async def test_move_child_widget_in_relation_to_itself_index(reference: str) -> "after", ], ) +@pytest.mark.anyio async def test_move_child_widget_in_relation_to_itself_widget(reference: str) -> None: """Regression test for https://github.com/Textualize/textual/issues/1743""" @@ -114,6 +122,7 @@ async def test_move_child_widget_in_relation_to_itself_widget(reference: str) -> pilot.app.screen.move_child(child, **kwargs) # Shouldn't raise an error. +@pytest.mark.anyio async def test_move_past_end_of_child_list() -> None: """Test attempting to move past the end of the child list.""" async with App().run_test() as pilot: @@ -124,6 +133,7 @@ async def test_move_past_end_of_child_list() -> None: container.move_child(widgets[0], before=len(widgets) + 10) +@pytest.mark.anyio async def test_move_before_end_of_child_list() -> None: """Test attempting to move before the end of the child list.""" async with App().run_test() as pilot: @@ -134,6 +144,7 @@ async def test_move_before_end_of_child_list() -> None: container.move_child(widgets[0], before=-(len(widgets) + 10)) +@pytest.mark.anyio async def test_move_before_permutations() -> None: """Test the different permutations of moving one widget before another.""" widgets = [Widget(id=f"widget-{n}") for n in range(10)] @@ -153,6 +164,7 @@ async def test_move_before_permutations() -> None: assert container._nodes[2].id == "widget-2" +@pytest.mark.anyio async def test_move_after_permutations() -> None: """Test the different permutations of moving one widget after another.""" widgets = [Widget(id=f"widget-{n}") for n in range(10)] @@ -169,6 +181,7 @@ async def test_move_after_permutations() -> None: assert container._nodes[2].id == "widget-2" +@pytest.mark.anyio async def test_move_child_after_last_child() -> None: """Test moving after a child after the last child.""" async with App().run_test() as pilot: @@ -180,6 +193,7 @@ async def test_move_child_after_last_child() -> None: assert container._nodes[-1].id == "widget-0" +@pytest.mark.anyio async def test_move_child_after_last_numeric_location() -> None: """Test moving after a child after the last child's numeric position.""" async with App().run_test() as pilot: diff --git a/tests/test_widget_mounting.py b/tests/test_widget_mounting.py index 79f7364ad6..e26b4c4866 100644 --- a/tests/test_widget_mounting.py +++ b/tests/test_widget_mounting.py @@ -13,6 +13,7 @@ def __init__(self) -> None: super().__init__(self) +@pytest.mark.anyio async def test_mount_via_app() -> None: """Perform mount tests via the app.""" @@ -116,6 +117,7 @@ async def test_mount_via_app() -> None: await pilot.app.mount(Static(), before="Static") +@pytest.mark.anyio async def test_mount_error() -> None: """Mounting a widget on an un-mounted widget should raise an error.""" app = App() diff --git a/tests/test_widget_removing.py b/tests/test_widget_removing.py index ce352857f5..ff7ed7594b 100644 --- a/tests/test_widget_removing.py +++ b/tests/test_widget_removing.py @@ -3,6 +3,7 @@ from textual.widgets import Button, Label, Static +@pytest.mark.anyio async def test_remove_single_widget(): """It should be possible to the only widget on a screen.""" async with App().run_test() as pilot: @@ -16,6 +17,7 @@ async def test_remove_single_widget(): assert len(pilot.app.screen._nodes) == 0 +@pytest.mark.anyio async def test_many_remove_all_widgets(): """It should be possible to remove all widgets on a multi-widget screen.""" async with App().run_test() as pilot: @@ -25,6 +27,7 @@ async def test_many_remove_all_widgets(): assert len(pilot.app.screen._nodes) == 0 +@pytest.mark.anyio async def test_many_remove_some_widgets(): """It should be possible to remove some widgets on a multi-widget screen.""" async with App().run_test() as pilot: @@ -34,6 +37,7 @@ async def test_many_remove_some_widgets(): assert len(pilot.app.screen._nodes) == 5 +@pytest.mark.anyio async def test_remove_branch(): """It should be possible to remove a whole branch in the DOM.""" async with App().run_test() as pilot: @@ -47,6 +51,7 @@ async def test_remove_branch(): assert len(pilot.app.screen.walk_children(with_self=False)) == 7 +@pytest.mark.anyio async def test_remove_overlap(): """It should be possible to remove an overlapping collection of widgets.""" async with App().run_test() as pilot: @@ -60,6 +65,7 @@ async def test_remove_overlap(): assert len(pilot.app.screen.walk_children(with_self=False)) == 1 +@pytest.mark.anyio async def test_remove_move_focus(): """Removing a focused widget should settle focus elsewhere.""" async with App().run_test() as pilot: @@ -78,6 +84,7 @@ async def test_remove_move_focus(): assert pilot.app.focused == buttons[9] +@pytest.mark.anyio async def test_widget_remove_order() -> None: """A Widget.remove of a top-level widget should cause bottom-first removal.""" @@ -97,6 +104,7 @@ def on_unmount(self, _): assert removals == ["grandchild", "child", "parent"] +@pytest.mark.anyio async def test_query_remove_order(): """A DOMQuery.remove of a top-level widget should cause bottom-first removal.""" @@ -125,6 +133,7 @@ def compose(self) -> ComposeResult: yield Label(str(index)) +@pytest.mark.anyio async def test_widget_remove_children_container(): app = ExampleApp() async with app.run_test(): @@ -141,6 +150,7 @@ async def test_widget_remove_children_container(): assert len(container.children) == 0 +@pytest.mark.anyio async def test_widget_remove_children_with_star_selector(): app = ExampleApp() async with app.run_test(): @@ -157,6 +167,7 @@ async def test_widget_remove_children_with_star_selector(): assert len(container.children) == 0 +@pytest.mark.anyio async def test_widget_remove_children_with_string_selector(): app = ExampleApp() async with app.run_test(): @@ -174,6 +185,7 @@ async def test_widget_remove_children_with_string_selector(): assert len(app.query(Label)) == 5 +@pytest.mark.anyio async def test_widget_remove_children_with_type_selector(): app = ExampleApp() async with app.run_test(): @@ -182,6 +194,7 @@ async def test_widget_remove_children_with_type_selector(): assert len(app.query(Button)) == 0 +@pytest.mark.anyio async def test_widget_remove_children_with_selector_does_not_leak(): app = ExampleApp() async with app.run_test(): @@ -198,6 +211,7 @@ async def test_widget_remove_children_with_selector_does_not_leak(): assert len(container.children) == 0 +@pytest.mark.anyio async def test_widget_remove_children_no_children(): app = ExampleApp() async with app.run_test(): @@ -213,6 +227,7 @@ async def test_widget_remove_children_no_children(): ) # No widgets have been removed, since Button has no children. +@pytest.mark.anyio async def test_widget_remove_children_no_children_match_selector(): app = ExampleApp() async with app.run_test(): diff --git a/tests/test_widget_visibility.py b/tests/test_widget_visibility.py index c625771ff0..6e98140f7c 100644 --- a/tests/test_widget_visibility.py +++ b/tests/test_widget_visibility.py @@ -2,6 +2,7 @@ from textual.widgets import Label +@pytest.mark.anyio async def test_hide() -> None: """Check that setting visibility produces Hide messages.""" # https://github.com/Textualize/textual/issues/3460 diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index d4f8241ed3..8531b6986e 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -36,6 +36,7 @@ def compose(self) -> ComposeResult: yield text_area +@pytest.mark.anyio async def test_insert_text_start_maintain_selection_offset(): """Ensure that we can maintain the offset between the location an insert happens and the location of the selection.""" @@ -48,6 +49,7 @@ async def test_insert_text_start_maintain_selection_offset(): assert text_area.selection == Selection.cursor((0, 10)) +@pytest.mark.anyio async def test_insert_text_start(): """The document is correctly updated on inserting at the start. If we don't maintain the selection offset, the cursor jumps @@ -61,6 +63,7 @@ async def test_insert_text_start(): assert text_area.selection == Selection.cursor((0, 5)) +@pytest.mark.anyio async def test_insert_empty_string(): app = TextAreaApp() async with app.run_test(): @@ -72,6 +75,7 @@ async def test_insert_empty_string(): assert text_area.text == "0123456789" +@pytest.mark.anyio async def test_replace_empty_string(): app = TextAreaApp() async with app.run_test(): @@ -92,6 +96,7 @@ async def test_replace_empty_string(): ((0, 3), (0, 5), (0, 3)), # API insert just after cursor ], ) +@pytest.mark.anyio async def test_insert_character_near_cursor_maintain_selection_offset( cursor_location, insert_location, @@ -114,6 +119,7 @@ async def test_insert_character_near_cursor_maintain_selection_offset( ((0, 0), (1, 0), (0, 0)), # API insert after cursor row ], ) +@pytest.mark.anyio async def test_insert_newline_around_cursor_maintain_selection_offset( cursor_location, insert_location, cursor_destination ): @@ -125,6 +131,7 @@ async def test_insert_newline_around_cursor_maintain_selection_offset( assert text_area.selection == Selection.cursor(cursor_destination) +@pytest.mark.anyio async def test_insert_newlines_start(): app = TextAreaApp() async with app.run_test(): @@ -134,6 +141,7 @@ async def test_insert_newlines_start(): assert text_area.selection == Selection.cursor((3, 0)) +@pytest.mark.anyio async def test_insert_newlines_end(): app = TextAreaApp() async with app.run_test(): @@ -142,6 +150,7 @@ async def test_insert_newlines_end(): assert text_area.text == TEXT + "\n\n\n" +@pytest.mark.anyio async def test_insert_windows_newlines(): app = TextAreaApp() async with app.run_test(): @@ -153,6 +162,7 @@ async def test_insert_windows_newlines(): assert text_area.text == "\n\n\n" + TEXT +@pytest.mark.anyio async def test_insert_old_mac_newlines(): app = TextAreaApp() async with app.run_test(): @@ -161,6 +171,7 @@ async def test_insert_old_mac_newlines(): assert text_area.text == "\n\n\n" + TEXT +@pytest.mark.anyio async def test_insert_text_non_cursor_location(): app = TextAreaApp() async with app.run_test(): @@ -170,6 +181,7 @@ async def test_insert_text_non_cursor_location(): assert text_area.selection == Selection.cursor((0, 0)) +@pytest.mark.anyio async def test_insert_text_non_cursor_location_dont_maintain_offset(): app = TextAreaApp() async with app.run_test(): @@ -193,6 +205,7 @@ async def test_insert_text_non_cursor_location_dont_maintain_offset(): assert text_area.selection == Selection.cursor((4, 5)) +@pytest.mark.anyio async def test_insert_multiline_text(): app = TextAreaApp() async with app.run_test(): @@ -210,6 +223,7 @@ async def test_insert_multiline_text(): assert text_area.text == expected_content +@pytest.mark.anyio async def test_insert_multiline_text_maintain_offset(): app = TextAreaApp() async with app.run_test(): @@ -236,6 +250,7 @@ async def test_insert_multiline_text_maintain_offset(): assert text_area.text == expected_content +@pytest.mark.anyio async def test_replace_multiline_text(): app = TextAreaApp() async with app.run_test(): @@ -262,6 +277,7 @@ async def test_replace_multiline_text(): assert text_area.text == expected_content +@pytest.mark.anyio async def test_replace_multiline_text_maintain_selection(): app = TextAreaApp() async with app.run_test(): @@ -296,6 +312,7 @@ async def test_replace_multiline_text_maintain_selection(): assert text_area.text == expected_content +@pytest.mark.anyio async def test_delete_within_line(): app = TextAreaApp() async with app.run_test(): @@ -325,6 +342,7 @@ async def test_delete_within_line(): assert text_area.text == expected_text +@pytest.mark.anyio async def test_delete_within_line_dont_maintain_offset(): app = TextAreaApp() async with app.run_test(): @@ -340,6 +358,7 @@ async def test_delete_within_line_dont_maintain_offset(): assert text_area.text == expected_text +@pytest.mark.anyio async def test_delete_multiple_lines_selection_above(): app = TextAreaApp() async with app.run_test(): @@ -376,6 +395,7 @@ async def test_delete_multiple_lines_selection_above(): ) +@pytest.mark.anyio async def test_delete_empty_document(): app = TextAreaApp() async with app.run_test(): @@ -386,6 +406,7 @@ async def test_delete_empty_document(): assert text_area.text == "" +@pytest.mark.anyio async def test_clear(): app = TextAreaApp() async with app.run_test(): @@ -393,6 +414,7 @@ async def test_clear(): text_area.clear() +@pytest.mark.anyio async def test_clear_empty_document(): app = TextAreaApp() async with app.run_test(): @@ -408,6 +430,7 @@ async def test_clear_empty_document(): [(2, 1), (0, 3)], # Ensuring independence from selection direction. ], ) +@pytest.mark.anyio async def test_insert_text_multiline_selection_top(select_from, select_to): """ An example to attempt to explain what we're testing here... @@ -465,6 +488,7 @@ async def test_insert_text_multiline_selection_top(select_from, select_to): [(2, 5), (0, 3)], # Ensuring independence from selection direction. ], ) +@pytest.mark.anyio async def test_insert_text_multiline_selection_bottom(select_from, select_to): """ The edited text is within the selected text on the bottom line @@ -505,6 +529,7 @@ async def test_insert_text_multiline_selection_bottom(select_from, select_to): assert text_area.text == "ABCDE\nFGHIJ\n*NO\nPQRST\nUVWXY\nZ\n" +@pytest.mark.anyio async def test_delete_fully_within_selection(): """User-facing selection should be best-effort adjusted when a programmatic replacement is made to the document.""" @@ -525,6 +550,7 @@ async def test_delete_fully_within_selection(): assert text_area.text == "01236789" +@pytest.mark.anyio async def test_replace_fully_within_selection(): """Adjust the selection when a replacement happens inside it.""" app = TextAreaApp() @@ -542,6 +568,7 @@ async def test_replace_fully_within_selection(): assert text_area.selected_text == "XX56" +@pytest.mark.anyio async def test_text_setter(): app = TextAreaApp() async with app.run_test(): @@ -551,6 +578,7 @@ async def test_text_setter(): assert text_area.text == new_text +@pytest.mark.anyio async def test_edits_on_read_only_mode(): """API edits should still be permitted on read-only mode.""" app = TextAreaApp() diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index f6b3f013f4..52645be412 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -36,6 +36,7 @@ def compose(self) -> ComposeResult: yield text_area +@pytest.mark.anyio async def test_single_keypress_printable_character(): app = TextAreaApp() async with app.run_test() as pilot: @@ -44,6 +45,7 @@ async def test_single_keypress_printable_character(): assert text_area.text == "x" + TEXT +@pytest.mark.anyio async def test_single_keypress_enter(): app = TextAreaApp() async with app.run_test() as pilot: @@ -66,6 +68,7 @@ async def test_single_keypress_enter(): ("💩💩", 2, 6), ], ) +@pytest.mark.anyio async def test_tab_with_spaces_goes_to_tab_stop( content, cursor_column, cursor_destination ): @@ -81,6 +84,7 @@ async def test_tab_with_spaces_goes_to_tab_stop( assert text_area.cursor_location[1] == cursor_destination +@pytest.mark.anyio async def test_delete_left(): app = TextAreaApp() async with app.run_test() as pilot: @@ -92,6 +96,7 @@ async def test_delete_left(): assert text_area.selection == Selection.cursor((0, 5)) +@pytest.mark.anyio async def test_delete_left_start(): app = TextAreaApp() async with app.run_test() as pilot: @@ -102,6 +107,7 @@ async def test_delete_left_start(): assert text_area.selection == Selection.cursor((0, 0)) +@pytest.mark.anyio async def test_delete_left_end(): app = TextAreaApp() async with app.run_test() as pilot: @@ -122,6 +128,7 @@ async def test_delete_left_end(): ("backspace", Selection((3, 4), (1, 2))), ], ) +@pytest.mark.anyio async def test_deletion_with_non_empty_selection(key, selection): """When there's a selection, pressing backspace or delete should delete everything that is selected and reset the selection to a cursor at the appropriate location.""" @@ -142,6 +149,7 @@ async def test_deletion_with_non_empty_selection(key, selection): ) +@pytest.mark.anyio async def test_delete_right(): """Pressing 'delete' deletes the character to the right of the cursor.""" app = TextAreaApp() @@ -154,6 +162,7 @@ async def test_delete_right(): assert text_area.selection == Selection.cursor((0, 13)) +@pytest.mark.anyio async def test_delete_right_end_of_line(): """Pressing 'delete' at the end of the line merges this line with the line below.""" app = TextAreaApp() @@ -177,6 +186,7 @@ async def test_delete_right_end_of_line(): (Selection((0, 4), (0, 2)), "01456789", "23", (0, 2)), ], ) +@pytest.mark.anyio async def test_cut(selection, expected_result, expected_clipboard, cursor_end_location): app = TextAreaApp() async with app.run_test() as pilot: @@ -204,6 +214,7 @@ async def test_cut(selection, expected_result, expected_clipboard, cursor_end_lo (Selection((1, 2), (2, 1)), "012\n3478\n9\n"), ], ) +@pytest.mark.anyio async def test_cut_multiline_document(selection, expected_result): app = TextAreaApp() async with app.run_test() as pilot: @@ -228,6 +239,7 @@ async def test_cut_multiline_document(selection, expected_result): (Selection((0, 4), (0, 2)), ""), ], ) +@pytest.mark.anyio async def test_delete_line(selection, expected_result): app = TextAreaApp() async with app.run_test() as pilot: @@ -263,6 +275,7 @@ async def test_delete_line(selection, expected_result): (Selection((0, 0), (4, 0)), ""), # delete all lines ], ) +@pytest.mark.anyio async def test_delete_line_multiline_document(selection, expected_result): app = TextAreaApp() async with app.run_test() as pilot: @@ -292,6 +305,7 @@ async def test_delete_line_multiline_document(selection, expected_result): (Selection((0, 5), (0, 2)), "01"), ], ) +@pytest.mark.anyio async def test_delete_to_end_of_line(selection, expected_result): app = TextAreaApp() async with app.run_test() as pilot: @@ -320,6 +334,7 @@ async def test_delete_to_end_of_line(selection, expected_result): (Selection((0, 5), (0, 2)), "23456789"), ], ) +@pytest.mark.anyio async def test_delete_to_start_of_line(selection, expected_result): app = TextAreaApp() async with app.run_test() as pilot: @@ -349,6 +364,7 @@ async def test_delete_to_start_of_line(selection, expected_result): (Selection((0, 4), (0, 11)), " 01789", Selection.cursor((0, 4))), ], ) +@pytest.mark.anyio async def test_delete_word_left(selection, expected_result, final_selection): app = TextAreaApp() async with app.run_test() as pilot: @@ -378,6 +394,7 @@ async def test_delete_word_left(selection, expected_result, final_selection): (Selection((0, 4), (0, 11)), "\t0126789", Selection.cursor((0, 4))), ], ) +@pytest.mark.anyio async def test_delete_word_left_with_tabs(selection, expected_result, final_selection): app = TextAreaApp() async with app.run_test() as pilot: @@ -391,6 +408,7 @@ async def test_delete_word_left_with_tabs(selection, expected_result, final_sele assert text_area.selection == final_selection +@pytest.mark.anyio async def test_delete_word_left_to_start_of_line(): """If no word boundary found when we 'delete word left', then the deletion happens to the start of the line.""" @@ -406,6 +424,7 @@ async def test_delete_word_left_to_start_of_line(): assert text_area.selection == Selection.cursor((1, 0)) +@pytest.mark.anyio async def test_delete_word_left_at_line_start(): """If we're at the start of a line and we 'delete word left', the line merges with the line above (if possible).""" @@ -432,6 +451,7 @@ async def test_delete_word_left_at_line_start(): (Selection((0, 4), (0, 11)), " 01789", Selection.cursor((0, 4))), ], ) +@pytest.mark.anyio async def test_delete_word_right(selection, expected_result, final_selection): app = TextAreaApp() async with app.run_test() as pilot: @@ -445,6 +465,7 @@ async def test_delete_word_right(selection, expected_result, final_selection): assert text_area.selection == final_selection +@pytest.mark.anyio async def test_delete_word_right_delete_to_end_of_line(): app = TextAreaApp() async with app.run_test() as pilot: @@ -458,6 +479,7 @@ async def test_delete_word_right_delete_to_end_of_line(): assert text_area.selection == Selection.cursor((0, 3)) +@pytest.mark.anyio async def test_delete_word_right_at_end_of_line(): app = TextAreaApp() async with app.run_test() as pilot: @@ -486,6 +508,7 @@ async def test_delete_word_right_at_end_of_line(): "tab", ], ) +@pytest.mark.anyio async def test_edit_read_only_mode_does_nothing(binding): """Try out various key-presses and bindings and ensure they don't alter the document when read_only=True.""" @@ -509,6 +532,7 @@ async def test_edit_read_only_mode_does_nothing(binding): Selection(start=(3, 0), end=(1, 0)), ], ) +@pytest.mark.anyio async def test_replace_lines_with_fewer_lines(selection): app = TextAreaApp() async with app.run_test() as pilot: @@ -534,6 +558,7 @@ async def test_replace_lines_with_fewer_lines(selection): Selection(start=(3, 0), end=(1, 0)), ], ) +@pytest.mark.anyio async def test_paste(selection): app = TextAreaApp() async with app.run_test() as pilot: @@ -553,6 +578,7 @@ async def test_paste(selection): assert text_area.selection == Selection.cursor((1, 1)) +@pytest.mark.anyio async def test_paste_read_only_does_nothing(): app = TextAreaApp() async with app.run_test() as pilot: diff --git a/tests/text_area/test_escape_binding.py b/tests/text_area/test_escape_binding.py index 3a8c653e01..9374be26c0 100644 --- a/tests/text_area/test_escape_binding.py +++ b/tests/text_area/test_escape_binding.py @@ -18,6 +18,7 @@ def on_mount(self) -> None: self.push_screen(TextAreaDialog()) +@pytest.mark.anyio async def test_escape_key_when_tab_behavior_is_focus(): """Regression test for https://github.com/Textualize/textual/issues/4110 @@ -37,6 +38,7 @@ async def test_escape_key_when_tab_behavior_is_focus(): assert not isinstance(pilot.app.screen, TextAreaDialog) +@pytest.mark.anyio async def test_escape_key_when_tab_behavior_is_indent(): """When the `tab_behavior` of TextArea is indent rather than switch focus, pressing should instead shift focus. diff --git a/tests/text_area/test_history.py b/tests/text_area/test_history.py index 1d0c7a0b0b..ed39d06c85 100644 --- a/tests/text_area/test_history.py +++ b/tests/text_area/test_history.py @@ -56,6 +56,7 @@ async def text_area(pilot): return pilot.app.text_area +@pytest.mark.anyio async def test_simple_undo_redo(): app = TextAreaApp() async with app.run_test() as pilot: @@ -69,6 +70,7 @@ async def test_simple_undo_redo(): assert text_area.text == "123" +@pytest.mark.anyio async def test_undo_selection_retained(): # Select a range of text and press backspace. app = TextAreaApp() @@ -91,6 +93,7 @@ async def test_undo_selection_retained(): assert text_area.selection == Selection.cursor((0, 0)) +@pytest.mark.anyio async def test_undo_checkpoint_created_on_cursor_move(): app = TextAreaApp() async with app.run_test() as pilot: @@ -133,6 +136,7 @@ async def test_undo_checkpoint_created_on_cursor_move(): assert text_area.selection == checkpoint_three_selection +@pytest.mark.anyio async def test_setting_text_property_resets_history(): app = TextAreaApp() async with app.run_test() as pilot: @@ -148,6 +152,7 @@ async def test_setting_text_property_resets_history(): assert text_area.text == text +@pytest.mark.anyio async def test_edits_batched_by_time(): app = TextAreaApp() async with app.run_test() as pilot: @@ -172,6 +177,7 @@ async def test_edits_batched_by_time(): assert text_area.text == "" +@pytest.mark.anyio async def test_undo_checkpoint_character_limit_reached(): app = TextAreaApp() async with app.run_test() as pilot: @@ -186,6 +192,7 @@ async def test_undo_checkpoint_character_limit_reached(): assert text_area.text == "" +@pytest.mark.anyio async def test_redo_with_no_undo_is_noop(): app = TextAreaApp() async with app.run_test() as pilot: @@ -195,6 +202,7 @@ async def test_redo_with_no_undo_is_noop(): assert text_area.text == SIMPLE_TEXT +@pytest.mark.anyio async def test_undo_with_empty_undo_stack_is_noop(): app = TextAreaApp() async with app.run_test() as pilot: @@ -204,6 +212,7 @@ async def test_undo_with_empty_undo_stack_is_noop(): assert text_area.text == SIMPLE_TEXT +@pytest.mark.anyio async def test_redo_stack_cleared_on_edit(): app = TextAreaApp() async with app.run_test() as pilot: @@ -235,6 +244,7 @@ async def test_redo_stack_cleared_on_edit(): assert text_area.selection == Selection.cursor((0, 1)) +@pytest.mark.anyio async def test_inserts_not_batched_with_deletes(): # 3 batches here: __1___ ___________2____________ __3__ @@ -259,6 +269,7 @@ async def test_inserts_not_batched_with_deletes(): assert text_area.text == "" +@pytest.mark.anyio async def test_paste_is_an_isolated_batch(): app = TextAreaApp() async with app.run_test() as pilot: @@ -282,6 +293,7 @@ async def test_paste_is_an_isolated_batch(): assert text_area.text == "" +@pytest.mark.anyio async def test_focus_creates_checkpoint(): app = TextAreaApp() async with app.run_test() as pilot: @@ -298,6 +310,7 @@ async def test_focus_creates_checkpoint(): assert text_area.text == "123" +@pytest.mark.anyio async def test_undo_redo_deletions_batched(): app = TextAreaApp() async with app.run_test() as pilot: @@ -347,6 +360,7 @@ async def test_undo_redo_deletions_batched(): assert text_area.selection == Selection.cursor((0, 0)) +@pytest.mark.anyio async def test_max_checkpoints(): app = TextAreaApp() async with app.run_test() as pilot: @@ -362,6 +376,7 @@ async def test_max_checkpoints(): assert len(text_area.history.undo_stack) == MAX_CHECKPOINTS +@pytest.mark.anyio async def test_redo_stack(): app = TextAreaApp() async with app.run_test() as pilot: @@ -385,6 +400,7 @@ async def test_redo_stack(): assert len(text_area.history.redo_stack) == 0 +@pytest.mark.anyio async def test_backward_selection_undo_redo(): app = TextAreaApp() async with app.run_test() as pilot: diff --git a/tests/text_area/test_issue_4301.py b/tests/text_area/test_issue_4301.py index b149a5f360..bc97d7610f 100644 --- a/tests/text_area/test_issue_4301.py +++ b/tests/text_area/test_issue_4301.py @@ -23,6 +23,7 @@ def compose(self) -> ComposeResult: ["A", "delete", "backspace"], ), ) +@pytest.mark.anyio async def test_issue_4301_reproduction(selection: Selection, edit: str) -> None: """Test https://github.com/Textualize/textual/issues/4301""" diff --git a/tests/text_area/test_languages.py b/tests/text_area/test_languages.py index f5f381354f..29956e111f 100644 --- a/tests/text_area/test_languages.py +++ b/tests/text_area/test_languages.py @@ -10,6 +10,7 @@ def compose(self) -> ComposeResult: yield TextArea("print('hello')", language="python") +@pytest.mark.anyio async def test_setting_builtin_language_via_constructor(): class MyTextAreaApp(App): def compose(self) -> ComposeResult: @@ -25,6 +26,7 @@ def compose(self) -> ComposeResult: assert text_area.language == "markdown" +@pytest.mark.anyio async def test_setting_builtin_language_via_attribute(): class MyTextAreaApp(App): def compose(self) -> ComposeResult: @@ -42,6 +44,7 @@ def compose(self) -> ComposeResult: assert text_area.language == "markdown" +@pytest.mark.anyio async def test_setting_language_to_none(): app = TextAreaApp() async with app.run_test(): @@ -51,6 +54,7 @@ async def test_setting_language_to_none(): @pytest.mark.syntax +@pytest.mark.anyio async def test_setting_unknown_language(): app = TextAreaApp() async with app.run_test(): @@ -61,6 +65,7 @@ async def test_setting_unknown_language(): @pytest.mark.syntax +@pytest.mark.anyio async def test_register_language(): app = TextAreaApp() @@ -80,6 +85,7 @@ async def test_register_language(): @pytest.mark.syntax +@pytest.mark.anyio async def test_update_highlight_query(): app = TextAreaApp() async with app.run_test(): diff --git a/tests/text_area/test_messages.py b/tests/text_area/test_messages.py index 6f91fb0db2..49f98b1597 100644 --- a/tests/text_area/test_messages.py +++ b/tests/text_area/test_messages.py @@ -35,6 +35,7 @@ def get_selection_changed_messages( ] +@pytest.mark.anyio async def test_changed_message_edit_via_api(): app = TextAreaApp() async with app.run_test() as pilot: @@ -50,6 +51,7 @@ async def test_changed_message_edit_via_api(): ] +@pytest.mark.anyio async def test_changed_message_via_typing(): app = TextAreaApp() async with app.run_test() as pilot: @@ -64,6 +66,7 @@ async def test_changed_message_via_typing(): ] +@pytest.mark.anyio async def test_changed_message_edit_via_assignment(): app = TextAreaApp() async with app.run_test() as pilot: @@ -77,6 +80,7 @@ async def test_changed_message_edit_via_assignment(): assert get_selection_changed_messages(app.messages) == [] +@pytest.mark.anyio async def test_selection_changed_via_api(): app = TextAreaApp() async with app.run_test() as pilot: @@ -91,6 +95,7 @@ async def test_selection_changed_via_api(): ] +@pytest.mark.anyio async def test_selection_changed_via_typing(): app = TextAreaApp() async with app.run_test() as pilot: diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 0efc9ca62d..3f780855c8 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -18,6 +18,7 @@ def compose(self) -> ComposeResult: yield text_area +@pytest.mark.anyio async def test_default_selection(): """The cursor starts at (0, 0) in the document.""" app = TextAreaApp() @@ -26,6 +27,7 @@ async def test_default_selection(): assert text_area.selection == Selection.cursor((0, 0)) +@pytest.mark.anyio async def test_cursor_location_get(): app = TextAreaApp() async with app.run_test(): @@ -34,6 +36,7 @@ async def test_cursor_location_get(): assert text_area.cursor_location == (2, 2) +@pytest.mark.anyio async def test_cursor_location_set(): app = TextAreaApp() async with app.run_test(): @@ -43,6 +46,7 @@ async def test_cursor_location_set(): assert text_area.selection == Selection.cursor(target) +@pytest.mark.anyio async def test_cursor_location_set_while_selecting(): """If you set the cursor_location while a selection is in progress, the start/anchor point of the selection will remain where it is.""" @@ -55,6 +59,7 @@ async def test_cursor_location_set_while_selecting(): assert text_area.selection == Selection((0, 0), target) +@pytest.mark.anyio async def test_move_cursor_select(): app = TextAreaApp() async with app.run_test(): @@ -64,6 +69,7 @@ async def test_move_cursor_select(): assert text_area.selection == Selection((1, 1), (2, 3)) +@pytest.mark.anyio async def test_move_cursor_relative(): app = TextAreaApp() async with app.run_test(): @@ -79,6 +85,7 @@ async def test_move_cursor_relative(): assert text_area.selection == Selection.cursor((4, 0)) +@pytest.mark.anyio async def test_selected_text_forward(): """Selecting text from top to bottom results in the correct selected_text.""" app = TextAreaApp() @@ -94,6 +101,7 @@ async def test_selected_text_forward(): ) +@pytest.mark.anyio async def test_selected_text_backward(): """Selecting text from bottom to top results in the correct selected_text.""" app = TextAreaApp() @@ -109,6 +117,7 @@ async def test_selected_text_backward(): ) +@pytest.mark.anyio async def test_selected_text_multibyte(): app = TextAreaApp() async with app.run_test(): @@ -118,6 +127,7 @@ async def test_selected_text_multibyte(): assert text_area.selected_text == "んに" +@pytest.mark.anyio async def test_selection_clamp(): """When you set the selection reactive, it's clamped to within the document bounds.""" app = TextAreaApp() @@ -135,6 +145,7 @@ async def test_selection_clamp(): ((1, 0), (0, 16)), ], ) +@pytest.mark.anyio async def test_get_cursor_left_location(start, end): app = TextAreaApp() async with app.run_test(): @@ -152,6 +163,7 @@ async def test_get_cursor_left_location(start, end): ((4, 0), (4, 0)), ], ) +@pytest.mark.anyio async def test_get_cursor_right_location(start, end): app = TextAreaApp() async with app.run_test(): @@ -168,6 +180,7 @@ async def test_get_cursor_right_location(start, end): ((2, 56), (1, 24)), # snap to end of row above ], ) +@pytest.mark.anyio async def test_get_cursor_up_location(start, end): app = TextAreaApp() async with app.run_test(): @@ -187,6 +200,7 @@ async def test_get_cursor_up_location(start, end): ((2, 56), (3, 20)), # snap to end of row below ], ) +@pytest.mark.anyio async def test_get_cursor_down_location(start, end): app = TextAreaApp() async with app.run_test(): @@ -219,6 +233,7 @@ async def test_get_cursor_down_location(start, end): ((1, 14), (1, 11)), ], ) +@pytest.mark.anyio async def test_cursor_word_left_location(start, end): app = TextAreaApp() async with app.run_test(): @@ -249,6 +264,7 @@ async def test_cursor_word_left_location(start, end): ((1, 14), (1, 14)), ], ) +@pytest.mark.anyio async def test_cursor_word_right_location(start, end): app = TextAreaApp() async with app.run_test(): @@ -266,6 +282,7 @@ async def test_cursor_word_right_location(start, end): ("", Selection((0, 0), (0, 0))), ], ) +@pytest.mark.anyio async def test_select_all(content, expected_selection): app = TextAreaApp() async with app.run_test(): @@ -287,6 +304,7 @@ async def test_select_all(content, expected_selection): (0, "", Selection((0, 0), (0, 0))), ], ) +@pytest.mark.anyio async def test_select_line(index, content, expected_selection): app = TextAreaApp() async with app.run_test(): @@ -298,6 +316,7 @@ async def test_select_line(index, content, expected_selection): assert text_area.selection == expected_selection +@pytest.mark.anyio async def test_cursor_screen_offset_and_terminal_cursor_position_update(): class TextAreaCursorScreenOffset(App): def compose(self) -> ComposeResult: @@ -318,6 +337,7 @@ def compose(self) -> ComposeResult: assert app.cursor_position == (6, 2) +@pytest.mark.anyio async def test_cursor_screen_offset_and_terminal_cursor_position_scrolling(): class TextAreaCursorScreenOffset(App): def compose(self) -> ComposeResult: @@ -336,6 +356,7 @@ def compose(self) -> ComposeResult: assert app.cursor_position == (5, 1) +@pytest.mark.anyio async def test_mouse_selection_with_tab_characters(): """Regression test for https://github.com/Textualize/textual/issues/5212""" diff --git a/tests/text_area/test_selection_bindings.py b/tests/text_area/test_selection_bindings.py index 5985635cc6..6cebf1bd56 100644 --- a/tests/text_area/test_selection_bindings.py +++ b/tests/text_area/test_selection_bindings.py @@ -29,6 +29,7 @@ async def app(request): return TextAreaApp(read_only=request.param) +@pytest.mark.anyio async def test_mouse_click(app: TextAreaApp): """When you click the TextArea, the cursor moves to the expected location.""" async with app.run_test() as pilot: @@ -37,6 +38,7 @@ async def test_mouse_click(app: TextAreaApp): assert text_area.selection == Selection.cursor((1, 0)) +@pytest.mark.anyio async def test_mouse_click_clamp_from_right(app: TextAreaApp): """When you click to the right of the document bounds, the cursor is clamped to within the document bounds.""" @@ -46,6 +48,7 @@ async def test_mouse_click_clamp_from_right(app: TextAreaApp): assert text_area.selection == Selection.cursor((4, 0)) +@pytest.mark.anyio async def test_mouse_click_gutter_clamp(app: TextAreaApp): """When you click the gutter, it selects the start of the line.""" async with app.run_test() as pilot: @@ -54,6 +57,7 @@ async def test_mouse_click_gutter_clamp(app: TextAreaApp): assert text_area.selection == Selection.cursor((2, 0)) +@pytest.mark.anyio async def test_cursor_movement_basic(): app = TextAreaApp() async with app.run_test() as pilot: @@ -73,6 +77,7 @@ async def test_cursor_movement_basic(): assert text_area.selection == Selection.cursor((0, 0)) +@pytest.mark.anyio async def test_cursor_selection_right(app: TextAreaApp): """When you press shift+right the selection is updated correctly.""" async with app.run_test() as pilot: @@ -81,6 +86,7 @@ async def test_cursor_selection_right(app: TextAreaApp): assert text_area.selection == Selection((0, 0), (0, 3)) +@pytest.mark.anyio async def test_cursor_selection_right_to_previous_line(app: TextAreaApp): """When you press shift+right resulting in the cursor moving to the next line, the selection is updated correctly.""" @@ -91,6 +97,7 @@ async def test_cursor_selection_right_to_previous_line(app: TextAreaApp): assert text_area.selection == Selection((0, 15), (1, 2)) +@pytest.mark.anyio async def test_cursor_selection_left(app: TextAreaApp): """When you press shift+left the selection is updated correctly.""" async with app.run_test() as pilot: @@ -100,6 +107,7 @@ async def test_cursor_selection_left(app: TextAreaApp): assert text_area.selection == Selection((2, 5), (2, 2)) +@pytest.mark.anyio async def test_cursor_selection_left_to_previous_line(app: TextAreaApp): """When you press shift+left resulting in the cursor moving back to the previous line, the selection is updated correctly.""" @@ -113,6 +121,7 @@ async def test_cursor_selection_left_to_previous_line(app: TextAreaApp): assert text_area.selection == Selection((2, 2), (1, end_of_previous_line)) +@pytest.mark.anyio async def test_cursor_selection_up(app: TextAreaApp): """When you press shift+up the selection is updated correctly.""" async with app.run_test() as pilot: @@ -123,6 +132,7 @@ async def test_cursor_selection_up(app: TextAreaApp): assert text_area.selection == Selection((2, 3), (1, 3)) +@pytest.mark.anyio async def test_cursor_selection_up_when_cursor_on_first_line(app: TextAreaApp): """When you press shift+up the on the first line, it selects to the start.""" async with app.run_test() as pilot: @@ -135,6 +145,7 @@ async def test_cursor_selection_up_when_cursor_on_first_line(app: TextAreaApp): assert text_area.selection == Selection((0, 4), (0, 0)) +@pytest.mark.anyio async def test_cursor_selection_down(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) @@ -144,6 +155,7 @@ async def test_cursor_selection_down(app: TextAreaApp): assert text_area.selection == Selection((2, 5), (3, 5)) +@pytest.mark.anyio async def test_cursor_selection_down_when_cursor_on_last_line(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) @@ -156,6 +168,7 @@ async def test_cursor_selection_down_when_cursor_on_last_line(app: TextAreaApp): assert text_area.selection == Selection((1, 2), (1, 5)) +@pytest.mark.anyio async def test_cursor_word_right(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) @@ -166,6 +179,7 @@ async def test_cursor_word_right(app: TextAreaApp): assert text_area.selection == Selection.cursor((0, 3)) +@pytest.mark.anyio async def test_cursor_word_right_select(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) @@ -176,6 +190,7 @@ async def test_cursor_word_right_select(app: TextAreaApp): assert text_area.selection == Selection((0, 0), (0, 3)) +@pytest.mark.anyio async def test_cursor_word_left(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) @@ -187,6 +202,7 @@ async def test_cursor_word_left(app: TextAreaApp): assert text_area.selection == Selection.cursor((0, 4)) +@pytest.mark.anyio async def test_cursor_word_left_select(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) @@ -199,6 +215,7 @@ async def test_cursor_word_left_select(app: TextAreaApp): @pytest.mark.parametrize("key", ["end", "ctrl+e"]) +@pytest.mark.anyio async def test_cursor_to_line_end(key, app: TextAreaApp): """You can use the keyboard to jump the cursor to the end of the current line.""" async with app.run_test() as pilot: @@ -211,6 +228,7 @@ async def test_cursor_to_line_end(key, app: TextAreaApp): @pytest.mark.parametrize("key", ["home", "ctrl+a"]) +@pytest.mark.anyio async def test_cursor_to_line_home_basic_behaviour(key, app: TextAreaApp): """You can use the keyboard to jump the cursor to the start of the current line.""" async with app.run_test() as pilot: @@ -232,6 +250,7 @@ async def test_cursor_to_line_home_basic_behaviour(key, app: TextAreaApp): ((0, 15), (0, 4)), ], ) +@pytest.mark.anyio async def test_cursor_line_home_smart_home( cursor_start, cursor_destination, app: TextAreaApp ): @@ -246,6 +265,7 @@ async def test_cursor_line_home_smart_home( assert text_area.selection == Selection.cursor(cursor_destination) +@pytest.mark.anyio async def test_cursor_page_down(app: TextAreaApp): """Pagedown moves the cursor down 1 page, retaining column index.""" async with app.run_test() as pilot: @@ -257,6 +277,7 @@ async def test_cursor_page_down(app: TextAreaApp): assert text_area.selection == Selection.cursor((app.size.height - margin, 1)) +@pytest.mark.anyio async def test_cursor_page_up(app: TextAreaApp): """Pageup moves the cursor up 1 page, retaining column index.""" async with app.run_test() as pilot: @@ -270,6 +291,7 @@ async def test_cursor_page_up(app: TextAreaApp): ) +@pytest.mark.anyio async def test_cursor_vertical_movement_visual_alignment_snapping(app: TextAreaApp): """When you move the cursor vertically, it should stay vertically aligned even when double-width characters are used.""" @@ -290,6 +312,7 @@ async def test_cursor_vertical_movement_visual_alignment_snapping(app: TextAreaA assert text_area.selection == Selection.cursor((1, 3)) +@pytest.mark.anyio async def test_select_line_binding(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) @@ -300,6 +323,7 @@ async def test_select_line_binding(app: TextAreaApp): assert text_area.selection == Selection((2, 0), (2, 56)) +@pytest.mark.anyio async def test_select_all_binding(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) diff --git a/tests/text_area/test_setting_themes.py b/tests/text_area/test_setting_themes.py index a3ee7aa35e..1a92c204b0 100644 --- a/tests/text_area/test_setting_themes.py +++ b/tests/text_area/test_setting_themes.py @@ -11,6 +11,7 @@ def compose(self) -> ComposeResult: yield TextArea("print('hello')", language="python") +@pytest.mark.anyio async def test_default_theme(): app = TextAreaApp() @@ -19,6 +20,7 @@ async def test_default_theme(): assert text_area.theme is "css" +@pytest.mark.anyio async def test_setting_builtin_themes(): class MyTextAreaApp(App[None]): def compose(self) -> ComposeResult: @@ -34,6 +36,7 @@ def compose(self) -> ComposeResult: assert text_area.theme == "monokai" +@pytest.mark.anyio async def test_setting_unknown_theme_raises_exception(): app = TextAreaApp() async with app.run_test(): @@ -42,6 +45,7 @@ async def test_setting_unknown_theme_raises_exception(): text_area.theme = "this-theme-doesnt-exist" +@pytest.mark.anyio async def test_registering_and_setting_theme(): app = TextAreaApp() diff --git a/tests/text_area/test_textarea_cut_copy_paste.py b/tests/text_area/test_textarea_cut_copy_paste.py index 3922eb41e2..1059442f6d 100644 --- a/tests/text_area/test_textarea_cut_copy_paste.py +++ b/tests/text_area/test_textarea_cut_copy_paste.py @@ -7,6 +7,7 @@ def compose(self) -> ComposeResult: yield TextArea() +@pytest.mark.anyio async def test_cut(): """Check that cut removes text and places it in the clipboard.""" app = TextAreaApp() @@ -20,6 +21,7 @@ async def test_cut(): assert app.clipboard == "rl" +@pytest.mark.anyio async def test_copy(): """Check that copy places text in the clipboard.""" app = TextAreaApp() @@ -33,6 +35,7 @@ async def test_copy(): assert app.clipboard == "rl" +@pytest.mark.anyio async def test_paste(): """Check that paste copies text from the local clipboard.""" app = TextAreaApp() diff --git a/tests/toggles/test_checkbox.py b/tests/toggles/test_checkbox.py index 095f874858..62bcf25630 100644 --- a/tests/toggles/test_checkbox.py +++ b/tests/toggles/test_checkbox.py @@ -20,6 +20,7 @@ def on_checkbox_changed(self, event: Checkbox.Changed) -> None: ) +@pytest.mark.anyio async def test_checkbox_initial_state() -> None: """The initial states of the check boxes should be as we specified.""" async with CheckboxApp().run_test() as pilot: @@ -32,6 +33,7 @@ async def test_checkbox_initial_state() -> None: assert pilot.app.events_received == [] +@pytest.mark.anyio async def test_checkbox_toggle() -> None: """Test the status of the check boxes after they've been toggled.""" async with CheckboxApp().run_test() as pilot: diff --git a/tests/toggles/test_labels.py b/tests/toggles/test_labels.py index 317fccabd1..70207a4bed 100644 --- a/tests/toggles/test_labels.py +++ b/tests/toggles/test_labels.py @@ -12,6 +12,7 @@ def compose(self) -> ComposeResult: yield RadioSet("Before") +@pytest.mark.anyio async def test_change_labels() -> None: """It should be possible to change the labels of toggle buttons.""" async with LabelChangeApp().run_test() as pilot: diff --git a/tests/toggles/test_radiobutton.py b/tests/toggles/test_radiobutton.py index 0dc4fda05e..2c67ad9cd7 100644 --- a/tests/toggles/test_radiobutton.py +++ b/tests/toggles/test_radiobutton.py @@ -24,6 +24,7 @@ def on_radio_button_changed(self, event: RadioButton.Changed) -> None: ) +@pytest.mark.anyio async def test_radio_button_initial_state() -> None: """The initial states of the radio buttons should be as we specified.""" async with RadioButtonApp().run_test() as pilot: @@ -40,6 +41,7 @@ async def test_radio_button_initial_state() -> None: assert pilot.app.events_received == [] +@pytest.mark.anyio async def test_radio_button_toggle() -> None: """Test the status of the radio buttons after they've been toggled.""" async with RadioButtonApp().run_test() as pilot: diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index 1bd6eeb1b0..e63038989c 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -27,6 +27,7 @@ def on_radio_set_changed(self, event: RadioSet.Changed) -> None: ) +@pytest.mark.anyio async def test_radio_sets_initial_state(): """The initial states of the radio sets should be as we specified.""" async with RadioSetApp().run_test() as pilot: @@ -37,6 +38,7 @@ async def test_radio_sets_initial_state(): assert pilot.app.events_received == [] +@pytest.mark.anyio async def test_click_sets_focus(): """Clicking within a radio set should set focus.""" async with RadioSetApp().run_test() as pilot: @@ -46,6 +48,7 @@ async def test_click_sets_focus(): assert pilot.app.screen.focused == pilot.app.query_one("#from_buttons") +@pytest.mark.anyio async def test_radio_sets_toggle(): """Test the status of the radio sets after they've been toggled.""" async with RadioSetApp().run_test() as pilot: @@ -62,6 +65,7 @@ async def test_radio_sets_toggle(): ] +@pytest.mark.anyio async def test_radioset_same_button_mash(): """Mashing the same button should have no effect.""" async with RadioSetApp().run_test() as pilot: @@ -71,6 +75,7 @@ async def test_radioset_same_button_mash(): assert pilot.app.events_received == [] +@pytest.mark.anyio async def test_radioset_inner_navigation(): """Using the cursor keys should navigate between buttons in a set.""" async with RadioSetApp().run_test() as pilot: @@ -96,6 +101,7 @@ async def test_radioset_inner_navigation(): assert pilot.app.query_one("#from_strings", RadioSet)._selected == 1 +@pytest.mark.anyio async def test_radioset_inner_navigation_post_build(): class EmptyRadioSetApp(App[None]): def compose(self) -> ComposeResult: @@ -113,6 +119,7 @@ def on_mount(self) -> None: assert pilot.app.query_one(RadioSet)._selected == 4 +@pytest.mark.anyio async def test_radioset_breakout_navigation(): """Shift/Tabbing while in a radioset should move to the previous/next focsuable after the set itself.""" async with RadioSetApp().run_test() as pilot: @@ -130,6 +137,7 @@ def compose(self) -> ComposeResult: yield RadioButton(str(n), True) +@pytest.mark.anyio async def test_there_can_only_be_one(): """Adding multiple 'on' buttons should result in only one on.""" async with BadRadioSetApp().run_test() as pilot: @@ -155,6 +163,7 @@ def on_radio_set_changed(self, radio_set: RadioSet.Changed) -> None: self.selected.append(str(radio_set.pressed.label)) +@pytest.mark.anyio async def test_keyboard_navigation_with_disabled_buttons(): """Regression test for https://github.com/Textualize/textual/issues/3839.""" diff --git a/tests/tree/test_directory_tree.py b/tests/tree/test_directory_tree.py index b06b8d5edc..f2ec397895 100644 --- a/tests/tree/test_directory_tree.py +++ b/tests/tree/test_directory_tree.py @@ -28,6 +28,7 @@ def record( self.messages.append(event.__class__.__name__) +@pytest.mark.anyio async def test_directory_tree_file_selected_message(tmp_path: Path) -> None: """Selecting a file should result in a file selected message being emitted.""" @@ -51,6 +52,7 @@ async def test_directory_tree_file_selected_message(tmp_path: Path) -> None: assert pilot.app.messages == ["FileSelected"] +@pytest.mark.anyio async def test_directory_tree_directory_selected_message(tmp_path: Path) -> None: """Selecting a directory should result in a directory selected message being emitted.""" @@ -82,6 +84,7 @@ async def test_directory_tree_directory_selected_message(tmp_path: Path) -> None assert pilot.app.messages == ["DirectorySelected", "DirectorySelected"] +@pytest.mark.anyio async def test_directory_tree_reload_node(tmp_path: Path) -> None: """Reloading a node of a directory tree should display newly created file inside the directory.""" @@ -126,6 +129,7 @@ async def test_directory_tree_reload_node(tmp_path: Path) -> None: ] +@pytest.mark.anyio async def test_directory_tree_reload_other_node(tmp_path: Path) -> None: """Reloading a node of a directory tree should not reload content of other directory.""" @@ -177,6 +181,7 @@ async def test_directory_tree_reload_other_node(tmp_path: Path) -> None: assert unaffected_node.children[0].label == Text(NOT_RELOADED_FILE3_NAME) +@pytest.mark.anyio async def test_directory_tree_reloading_preserves_state(tmp_path: Path) -> None: """Regression test for https://github.com/Textualize/textual/issues/4122. diff --git a/tests/tree/test_node_refresh.py b/tests/tree/test_node_refresh.py index 8efaaec1ed..a2fa7e203c 100644 --- a/tests/tree/test_node_refresh.py +++ b/tests/tree/test_node_refresh.py @@ -30,6 +30,7 @@ def on_mount(self) -> None: self.query_one(HistoryTree).root.expand_all() +@pytest.mark.anyio async def test_initial_state() -> None: """Initially all the visible nodes should have had a render call.""" app = RefreshApp() @@ -37,6 +38,7 @@ async def test_initial_state() -> None: assert app.query_one(HistoryTree).render_hits == {(0, 0), (1, 0), (2, 0)} +@pytest.mark.anyio async def test_root_refresh() -> None: """A refresh of the root node should cause a subsequent render call.""" async with RefreshApp().run_test() as pilot: @@ -47,6 +49,7 @@ async def test_root_refresh() -> None: assert (0, 1) in pilot.app.query_one(HistoryTree).render_hits +@pytest.mark.anyio async def test_child_refresh() -> None: """A refresh of the child node should cause a subsequent render call.""" async with RefreshApp().run_test() as pilot: @@ -57,6 +60,7 @@ async def test_child_refresh() -> None: assert (1, 1) in pilot.app.query_one(HistoryTree).render_hits +@pytest.mark.anyio async def test_grandchild_refresh() -> None: """A refresh of the grandchild node should cause a subsequent render call.""" async with RefreshApp().run_test() as pilot: diff --git a/tests/tree/test_tree_availability.py b/tests/tree/test_tree_availability.py index 39700901ac..a067326f06 100644 --- a/tests/tree/test_tree_availability.py +++ b/tests/tree/test_tree_availability.py @@ -53,6 +53,7 @@ def node_highlighted(self, event: Tree.NodeHighlighted[None]) -> None: self.record(event) +@pytest.mark.anyio async def test_creating_disabled_tree(): """Mounting a disabled `Tree` should result in the base `Widget` having a `disabled` property equal to `True`""" @@ -69,6 +70,7 @@ async def test_creating_disabled_tree(): assert tree.cursor_line == 0 +@pytest.mark.anyio async def test_creating_enabled_tree(): """Mounting an enabled `Tree` should result in the base `Widget` having a `disabled` property equal to `False`""" @@ -85,6 +87,7 @@ async def test_creating_enabled_tree(): assert tree.cursor_line == 1 +@pytest.mark.anyio async def test_disabled_tree_node_selected_message() -> None: """Clicking the root node disclosure triangle on a disabled tree should result in no messages being emitted.""" @@ -106,6 +109,7 @@ async def test_disabled_tree_node_selected_message() -> None: ] +@pytest.mark.anyio async def test_enabled_tree_node_selected_message() -> None: """Clicking the root node disclosure triangle on an enabled tree should result in an `NodeExpanded` message being emitted.""" diff --git a/tests/tree/test_tree_clearing.py b/tests/tree/test_tree_clearing.py index 02a2ce711d..cb7d6e0b75 100644 --- a/tests/tree/test_tree_clearing.py +++ b/tests/tree/test_tree_clearing.py @@ -43,6 +43,7 @@ def on_mount(self) -> None: node.add_leaf("Xiaojie", VerseMoon()) +@pytest.mark.anyio async def test_tree_simple_clear() -> None: """Clearing a tree should keep the old root label and data.""" async with TreeClearApp().run_test() as pilot: @@ -54,6 +55,7 @@ async def test_tree_simple_clear() -> None: assert isinstance(tree.root.data, VerseStar) +@pytest.mark.anyio async def test_tree_reset_with_label() -> None: """Resetting a tree with a new label should use the new label and set the data to None.""" async with TreeClearApp().run_test() as pilot: @@ -65,6 +67,7 @@ async def test_tree_reset_with_label() -> None: assert tree.root.data is None +@pytest.mark.anyio async def test_tree_reset_with_label_and_data() -> None: """Resetting a tree with a label and data have that label and data used.""" async with TreeClearApp().run_test() as pilot: @@ -76,6 +79,7 @@ async def test_tree_reset_with_label_and_data() -> None: assert isinstance(tree.root.data, VersePlanet) +@pytest.mark.anyio async def test_remove_node(): async with TreeClearApp().run_test() as pilot: tree = pilot.app.query_one(VerseTree) @@ -84,6 +88,7 @@ async def test_remove_node(): assert len(tree.root.children) == 1 +@pytest.mark.anyio async def test_remove_node_children(): async with TreeClearApp().run_test() as pilot: tree = pilot.app.query_one(VerseTree) @@ -94,6 +99,7 @@ async def test_remove_node_children(): assert len(tree.root.children[0].children) == 0 +@pytest.mark.anyio async def test_tree_remove_children_of_root(): """Test removing the children of the root.""" async with TreeClearApp().run_test() as pilot: @@ -103,6 +109,7 @@ async def test_tree_remove_children_of_root(): assert len(tree.root.children) == 0 +@pytest.mark.anyio async def test_attempt_to_remove_root(): """Attempting to remove the root should be an error.""" async with TreeClearApp().run_test() as pilot: diff --git a/tests/tree/test_tree_cursor.py b/tests/tree/test_tree_cursor.py index ada1a41c57..58fe0d87a2 100644 --- a/tests/tree/test_tree_cursor.py +++ b/tests/tree/test_tree_cursor.py @@ -39,6 +39,7 @@ def record_event( self.messages.append((event.__class__.__name__, event.node.id)) +@pytest.mark.anyio async def test_move_cursor() -> None: """Test moving the cursor to a node (updating the highlighted node).""" async with TreeApp().run_test() as pilot: @@ -56,6 +57,7 @@ async def test_move_cursor() -> None: ] +@pytest.mark.anyio async def test_move_cursor_reset() -> None: async with TreeApp().run_test() as pilot: app = pilot.app @@ -71,6 +73,7 @@ async def test_move_cursor_reset() -> None: ] +@pytest.mark.anyio async def test_select_node() -> None: async with TreeApp().run_test() as pilot: app = pilot.app @@ -85,6 +88,7 @@ async def test_select_node() -> None: ] +@pytest.mark.anyio async def test_select_node_reset() -> None: async with TreeApp().run_test() as pilot: app = pilot.app diff --git a/tests/tree/test_tree_expand_etc.py b/tests/tree/test_tree_expand_etc.py index a55a44e983..093647c10b 100644 --- a/tests/tree/test_tree_expand_etc.py +++ b/tests/tree/test_tree_expand_etc.py @@ -19,6 +19,7 @@ def on_mount(self) -> None: node = node.add(str(n)) +@pytest.mark.anyio async def test_tree_node_expand() -> None: """Expanding one node should not expand all nodes.""" async with TreeApp().run_test() as pilot: @@ -30,6 +31,7 @@ async def test_tree_node_expand() -> None: check_node = check_node.children[0] +@pytest.mark.anyio async def test_tree_node_expand_all() -> None: """Expanding all on a node should expand all child nodes too.""" async with TreeApp().run_test() as pilot: @@ -42,6 +44,7 @@ async def test_tree_node_expand_all() -> None: check_node = check_node.children[0] +@pytest.mark.anyio async def test_tree_node_collapse() -> None: """Collapsing one node should not collapse all nodes.""" async with TreeApp().run_test() as pilot: @@ -54,6 +57,7 @@ async def test_tree_node_collapse() -> None: check_node = check_node.children[0] +@pytest.mark.anyio async def test_tree_node_collapse_all() -> None: """Collapsing all on a node should collapse all child noes too.""" async with TreeApp().run_test() as pilot: @@ -67,6 +71,7 @@ async def test_tree_node_collapse_all() -> None: check_node = check_node.children[0] +@pytest.mark.anyio async def test_tree_node_toggle() -> None: """Toggling one node should not toggle all nodes.""" async with TreeApp().run_test() as pilot: @@ -83,6 +88,7 @@ async def test_tree_node_toggle() -> None: check_node = check_node.children[0] +@pytest.mark.anyio async def test_tree_node_toggle_all() -> None: """Toggling all on a node should toggle all child nodes too.""" async with TreeApp().run_test() as pilot: diff --git a/tests/tree/test_tree_messages.py b/tests/tree/test_tree_messages.py index f81b1ff4ad..ff2e40daf1 100644 --- a/tests/tree/test_tree_messages.py +++ b/tests/tree/test_tree_messages.py @@ -52,6 +52,7 @@ def on_tree_node_highlighted(self, event: Tree.NodeHighlighted[None]) -> None: self.record(event) +@pytest.mark.anyio async def test_tree_node_selected_message() -> None: """Selecting a node should result in a selected message being emitted.""" async with TreeApp().run_test() as pilot: @@ -64,6 +65,7 @@ async def test_tree_node_selected_message() -> None: ] +@pytest.mark.anyio async def test_tree_node_selected_message_no_auto() -> None: """Selecting a node should result in only a selected message being emitted.""" async with TreeApp().run_test() as pilot: @@ -76,6 +78,7 @@ async def test_tree_node_selected_message_no_auto() -> None: ] +@pytest.mark.anyio async def test_tree_node_expanded_message() -> None: """Expanding a node should result in an expanded message being emitted.""" async with TreeApp().run_test() as pilot: @@ -107,6 +110,7 @@ async def tree_node_all_expanded_by_code_message() -> None: ] +@pytest.mark.anyio async def test_tree_node_collapsed_message() -> None: """Collapsing a node should result in a collapsed message being emitted.""" async with TreeApp().run_test() as pilot: @@ -163,6 +167,7 @@ async def tree_node_all_toggled_by_code_message() -> None: ] +@pytest.mark.anyio async def test_tree_node_highlighted_message() -> None: """Highlighting a node should result in a highlighted message being emitted.""" async with TreeApp().run_test() as pilot: @@ -234,6 +239,7 @@ def on_tree_node_highlighted(self, event: Tree.NodeHighlighted[None]) -> None: self.record(event) +@pytest.mark.anyio async def test_expand_node_from_code() -> None: """Expanding a node from code should result in the appropriate message.""" async with TreeViaCodeApp(False).run_test() as pilot: @@ -244,6 +250,7 @@ async def test_expand_node_from_code() -> None: ] +@pytest.mark.anyio async def test_collapse_node_from_code() -> None: """Collapsing a node from code should result in the appropriate message.""" async with TreeViaCodeApp(True).run_test() as pilot: diff --git a/tests/workers/test_work_decorator.py b/tests/workers/test_work_decorator.py index a7d01fa187..2dfd945b85 100644 --- a/tests/workers/test_work_decorator.py +++ b/tests/workers/test_work_decorator.py @@ -55,16 +55,19 @@ async def work_with(launcher: Callable[[WorkApp], WorkType]) -> None: ] +@pytest.mark.anyio async def test_async_work() -> None: """It should be possible to decorate an async method as an async worker.""" await work_with(lambda app: app.async_work) +@pytest.mark.anyio async def test_async_thread_work() -> None: """It should be possible to decorate an async method as a thread worker.""" await work_with(lambda app: app.async_thread_work) +@pytest.mark.anyio async def test_thread_work() -> None: """It should be possible to decorate a non-async method as a thread worker.""" await work_with(lambda app: app.thread_work) @@ -170,6 +173,7 @@ def thread(self): ), ], ) +@pytest.mark.anyio async def test_calling_workers_from_within_workers(call_stack: Tuple[str]): """Regression test for https://github.com/Textualize/textual/issues/3472. diff --git a/tests/workers/test_worker.py b/tests/workers/test_worker.py index 61af91f638..d32e47e3fd 100644 --- a/tests/workers/test_worker.py +++ b/tests/workers/test_worker.py @@ -15,6 +15,7 @@ ) +@pytest.mark.anyio async def test_initialize(): """Test initial values.""" @@ -36,6 +37,7 @@ def foo() -> str: assert worker.result is None +@pytest.mark.anyio async def test_run_success() -> None: """Test successful runs.""" @@ -103,6 +105,7 @@ class RunApp(App): assert baz_thread_worker.result == "baz" +@pytest.mark.anyio async def test_run_error() -> None: async def run_error() -> str: await asyncio.sleep(0.1) @@ -120,6 +123,7 @@ class ErrorApp(App): await worker.wait() +@pytest.mark.anyio async def test_run_cancel() -> None: """Test run may be cancelled.""" @@ -141,6 +145,7 @@ class ErrorApp(App): await worker.wait() +@pytest.mark.anyio async def test_run_cancel_immediately() -> None: """Edge case for cancelling immediately.""" @@ -161,6 +166,7 @@ class ErrorApp(App): await worker.wait() +@pytest.mark.anyio async def test_get_worker() -> None: """Check get current worker.""" @@ -185,6 +191,7 @@ def test_no_active_worker() -> None: get_current_worker() +@pytest.mark.anyio async def test_progress_update(): async def long_work(): pass @@ -201,6 +208,7 @@ async def long_work(): assert worker.progress == 73 +@pytest.mark.anyio async def test_double_start(): async def long_work(): return 0 @@ -213,6 +221,7 @@ async def long_work(): assert await worker.wait() == 0 +@pytest.mark.anyio async def test_self_referential_deadlock(): async def self_referential_work(): await get_current_worker().wait() @@ -226,6 +235,7 @@ async def self_referential_work(): assert exc.type is DeadlockError +@pytest.mark.anyio async def test_wait_without_start(): async def work(): return diff --git a/tests/workers/test_worker_manager.py b/tests/workers/test_worker_manager.py index 7e81c734a0..b28d771173 100644 --- a/tests/workers/test_worker_manager.py +++ b/tests/workers/test_worker_manager.py @@ -15,6 +15,7 @@ def test_worker_manager_init(): assert list(reversed(app.workers)) == [] +@pytest.mark.anyio async def test_run_worker_async() -> None: """Check self.run_worker""" worker_events: list[Worker.StateChanged] = [] @@ -59,6 +60,7 @@ def compose(self) -> ComposeResult: ] +@pytest.mark.anyio async def test_run_worker_thread_non_async() -> None: """Check self.run_worker""" worker_events: list[Worker.StateChanged] = [] @@ -96,6 +98,7 @@ def compose(self) -> ComposeResult: ] +@pytest.mark.anyio async def test_run_worker_thread_async() -> None: """Check self.run_worker""" worker_events: list[Worker.StateChanged] = [] From cfb228b453b03cf78261217a4ebe493cc9b0f0b7 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 24 Apr 2025 10:29:01 +0100 Subject: [PATCH 13/14] import pytest for anyio mark --- docs/examples/guide/testing/test_rgb.py | 1 + tests/animations/test_disabling_animations.py | 2 ++ tests/animations/test_loading_indicator_animation.py | 2 ++ tests/animations/test_progress_bar_animation.py | 2 ++ tests/animations/test_scrolling_animation.py | 2 ++ tests/animations/test_switch_animation.py | 2 ++ tests/animations/test_tabs_underline_animation.py | 2 ++ tests/command_palette/test_click_away.py | 2 ++ tests/command_palette/test_command_source_environment.py | 2 ++ tests/command_palette/test_declare_sources.py | 2 ++ tests/command_palette/test_discover.py | 2 ++ tests/command_palette/test_escaping.py | 2 ++ tests/command_palette/test_events.py | 2 ++ tests/command_palette/test_interaction.py | 2 ++ tests/command_palette/test_no_results.py | 2 ++ tests/command_palette/test_run_on_select.py | 2 ++ tests/command_palette/test_worker_interference.py | 2 ++ tests/css/test_css_reloading.py | 2 ++ tests/css/test_initial.py | 2 ++ tests/css/test_screen_css.py | 2 ++ tests/directory_tree/test_change_path.py | 2 ++ tests/directory_tree/test_early_show_root.py | 2 ++ tests/footer/test_footer.py | 2 ++ tests/input/test_cut_copy_paste.py | 2 ++ tests/input/test_input_clear.py | 2 ++ tests/input/test_input_key_modification_actions.py | 2 ++ tests/input/test_input_key_movement_actions.py | 2 ++ tests/input/test_input_messages.py | 2 ++ tests/input/test_input_properties.py | 1 + tests/input/test_input_terminal_cursor.py | 2 ++ tests/input/test_select_on_focus.py | 2 ++ tests/listview/test_inherit_listview.py | 2 ++ tests/listview/test_listview_navigation.py | 2 ++ tests/notifications/test_all_levels_notifications.py | 2 ++ tests/notifications/test_app_notifications.py | 2 ++ tests/option_list/test_option_list_id_stability.py | 2 ++ tests/option_list/test_option_list_mouse_click.py | 2 ++ tests/option_list/test_option_list_mouse_hover.py | 2 ++ tests/option_list/test_option_list_option_subclass.py | 2 ++ tests/option_list/test_option_messages.py | 2 ++ tests/select/test_changed_message.py | 2 ++ tests/select/test_prompt.py | 1 + tests/select/test_remove.py | 2 ++ tests/selection_list/test_over_wide_selections.py | 2 ++ tests/selection_list/test_selection_click_checkbox.py | 2 ++ tests/selection_list/test_selection_messages.py | 2 ++ tests/selection_list/test_selection_values.py | 2 ++ tests/test_animation.py | 2 ++ tests/test_app_focus_blur.py | 2 ++ tests/test_await_remove.py | 2 ++ tests/test_binding_inheritance.py | 2 ++ tests/test_border_subtitle.py | 2 ++ tests/test_call_x_schedulers.py | 2 ++ tests/test_collapsible.py | 2 ++ tests/test_command.py | 2 ++ tests/test_compositor.py | 2 ++ tests/test_containers.py | 2 ++ tests/test_demo.py | 2 ++ tests/test_driver.py | 2 ++ tests/test_dynamic_bindings.py | 2 ++ tests/test_file_monitor.py | 2 ++ tests/test_focus.py | 2 ++ tests/test_header.py | 2 ++ tests/test_issue_4248.py | 2 ++ tests/test_keymap.py | 2 ++ tests/test_lazy.py | 2 ++ tests/test_links.py | 2 ++ tests/test_loading.py | 2 ++ tests/test_log.py | 2 ++ tests/test_message_handling.py | 2 ++ tests/test_modal.py | 2 ++ tests/test_mount.py | 2 ++ tests/test_overflow_change.py | 2 ++ tests/test_paste.py | 2 ++ tests/test_shutdown.py | 2 ++ tests/test_style_importance.py | 2 ++ tests/test_style_inheritance.py | 2 ++ tests/test_switch.py | 2 ++ tests/test_test_runner.py | 2 ++ tests/test_tooltips.py | 1 + tests/test_unmount.py | 2 ++ tests/test_visible.py | 2 ++ tests/test_widget_removing.py | 2 ++ tests/test_widget_visibility.py | 2 ++ tests/text_area/test_escape_binding.py | 2 ++ tests/text_area/test_messages.py | 2 ++ tests/text_area/test_textarea_cut_copy_paste.py | 2 ++ tests/toggles/test_checkbox.py | 2 ++ tests/toggles/test_labels.py | 2 ++ tests/toggles/test_radiobutton.py | 2 ++ tests/toggles/test_radioset.py | 2 ++ tests/tree/test_directory_tree.py | 1 + tests/tree/test_node_refresh.py | 1 + tests/tree/test_tree_availability.py | 2 ++ tests/tree/test_tree_cursor.py | 2 ++ tests/tree/test_tree_expand_etc.py | 2 ++ tests/tree/test_tree_messages.py | 2 ++ tests/workers/test_worker_manager.py | 2 ++ 98 files changed, 190 insertions(+) diff --git a/docs/examples/guide/testing/test_rgb.py b/docs/examples/guide/testing/test_rgb.py index dd4679aba8..42e01f005e 100644 --- a/docs/examples/guide/testing/test_rgb.py +++ b/docs/examples/guide/testing/test_rgb.py @@ -1,3 +1,4 @@ +import pytest from rgb import RGBApp from textual.color import Color diff --git a/tests/animations/test_disabling_animations.py b/tests/animations/test_disabling_animations.py index d60bdd25c9..f713697198 100644 --- a/tests/animations/test_disabling_animations.py +++ b/tests/animations/test_disabling_animations.py @@ -2,6 +2,8 @@ Test that generic animations can be disabled. """ +import pytest + from textual.app import App, ComposeResult from textual.color import Color from textual.widgets import Label diff --git a/tests/animations/test_loading_indicator_animation.py b/tests/animations/test_loading_indicator_animation.py index 0be388d620..93afac085e 100644 --- a/tests/animations/test_loading_indicator_animation.py +++ b/tests/animations/test_loading_indicator_animation.py @@ -3,6 +3,8 @@ (An animation that also plays on the level BASIC.) """ +import pytest + from textual.app import App diff --git a/tests/animations/test_progress_bar_animation.py b/tests/animations/test_progress_bar_animation.py index 24957cab6f..4685a96b2a 100644 --- a/tests/animations/test_progress_bar_animation.py +++ b/tests/animations/test_progress_bar_animation.py @@ -3,6 +3,8 @@ animation. (An animation that also plays on the level BASIC.) """ +import pytest + from textual.app import App, ComposeResult from textual.widgets import ProgressBar from textual.widgets._progress_bar import Bar diff --git a/tests/animations/test_scrolling_animation.py b/tests/animations/test_scrolling_animation.py index b77d5f8aaf..0eeee98147 100644 --- a/tests/animations/test_scrolling_animation.py +++ b/tests/animations/test_scrolling_animation.py @@ -3,6 +3,8 @@ (An animation that also plays on the level BASIC.) """ +import pytest + from textual.app import App, ComposeResult from textual.containers import VerticalScroll from textual.widgets import Label diff --git a/tests/animations/test_switch_animation.py b/tests/animations/test_switch_animation.py index f5b8cda8b3..048b3ddfdf 100644 --- a/tests/animations/test_switch_animation.py +++ b/tests/animations/test_switch_animation.py @@ -3,6 +3,8 @@ (An animation that also plays on the level BASIC.) """ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Switch diff --git a/tests/animations/test_tabs_underline_animation.py b/tests/animations/test_tabs_underline_animation.py index 07df0b7fcd..60c51d23e6 100644 --- a/tests/animations/test_tabs_underline_animation.py +++ b/tests/animations/test_tabs_underline_animation.py @@ -3,6 +3,8 @@ (An animation that also plays on the level BASIC.) """ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Label, TabbedContent, Tabs diff --git a/tests/command_palette/test_click_away.py b/tests/command_palette/test_click_away.py index e4ce749fb4..bacaa72ad6 100644 --- a/tests/command_palette/test_click_away.py +++ b/tests/command_palette/test_click_away.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.command import CommandPalette, Hit, Hits, Provider diff --git a/tests/command_palette/test_command_source_environment.py b/tests/command_palette/test_command_source_environment.py index bd299c0bff..30ca17e3d8 100644 --- a/tests/command_palette/test_command_source_environment.py +++ b/tests/command_palette/test_command_source_environment.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.command import Hit, Hits, Provider from textual.screen import Screen diff --git a/tests/command_palette/test_declare_sources.py b/tests/command_palette/test_declare_sources.py index 724887d93d..bc376f5c42 100644 --- a/tests/command_palette/test_declare_sources.py +++ b/tests/command_palette/test_declare_sources.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.command import CommandPalette, Hit, Hits, Provider from textual.screen import Screen diff --git a/tests/command_palette/test_discover.py b/tests/command_palette/test_discover.py index c506c5689c..1071d341c0 100644 --- a/tests/command_palette/test_discover.py +++ b/tests/command_palette/test_discover.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.command import CommandPalette, DiscoveryHit, Hit, Hits, Provider from textual.widgets import OptionList diff --git a/tests/command_palette/test_escaping.py b/tests/command_palette/test_escaping.py index 23ccc5249d..77391bda3a 100644 --- a/tests/command_palette/test_escaping.py +++ b/tests/command_palette/test_escaping.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.command import CommandPalette, Hit, Hits, Provider diff --git a/tests/command_palette/test_events.py b/tests/command_palette/test_events.py index 4aebb9b0cf..aa5ea65cf4 100644 --- a/tests/command_palette/test_events.py +++ b/tests/command_palette/test_events.py @@ -1,6 +1,8 @@ from typing import Union from unittest import mock +import pytest + from textual import on from textual.app import App from textual.command import CommandPalette, Hit, Hits, Provider diff --git a/tests/command_palette/test_interaction.py b/tests/command_palette/test_interaction.py index 5dc93db099..a5d6e403db 100644 --- a/tests/command_palette/test_interaction.py +++ b/tests/command_palette/test_interaction.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.command import CommandList, CommandPalette, Hit, Hits, Provider diff --git a/tests/command_palette/test_no_results.py b/tests/command_palette/test_no_results.py index f09a4efc00..75bbf905ac 100644 --- a/tests/command_palette/test_no_results.py +++ b/tests/command_palette/test_no_results.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.command import CommandPalette from textual.widgets import OptionList diff --git a/tests/command_palette/test_run_on_select.py b/tests/command_palette/test_run_on_select.py index 99e7167bc3..47d3995a3d 100644 --- a/tests/command_palette/test_run_on_select.py +++ b/tests/command_palette/test_run_on_select.py @@ -1,5 +1,7 @@ from functools import partial +import pytest + from textual.app import App from textual.command import CommandPalette, Hit, Hits, Provider from textual.widgets import Input diff --git a/tests/command_palette/test_worker_interference.py b/tests/command_palette/test_worker_interference.py index 3fa0624b73..b46f5eee04 100644 --- a/tests/command_palette/test_worker_interference.py +++ b/tests/command_palette/test_worker_interference.py @@ -2,6 +2,8 @@ from asyncio import sleep +import pytest + from textual import work from textual.app import App from textual.command import Hit, Hits, Provider diff --git a/tests/css/test_css_reloading.py b/tests/css/test_css_reloading.py index 1a29f80fe8..ca5db71ca7 100644 --- a/tests/css/test_css_reloading.py +++ b/tests/css/test_css_reloading.py @@ -1,6 +1,8 @@ import os from pathlib import Path +import pytest + from textual.app import App, ComposeResult from textual.screen import Screen from textual.widgets import Label diff --git a/tests/css/test_initial.py b/tests/css/test_initial.py index bbfc168a54..e8a10392d0 100644 --- a/tests/css/test_initial.py +++ b/tests/css/test_initial.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.color import Color from textual.widget import Widget diff --git a/tests/css/test_screen_css.py b/tests/css/test_screen_css.py index 524c2b4b74..fff846dd5f 100644 --- a/tests/css/test_screen_css.py +++ b/tests/css/test_screen_css.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.color import Color from textual.screen import Screen diff --git a/tests/directory_tree/test_change_path.py b/tests/directory_tree/test_change_path.py index b56e085f7c..4856c62b83 100644 --- a/tests/directory_tree/test_change_path.py +++ b/tests/directory_tree/test_change_path.py @@ -1,5 +1,7 @@ from pathlib import Path +import pytest + from textual.app import App, ComposeResult from textual.widgets import DirectoryTree diff --git a/tests/directory_tree/test_early_show_root.py b/tests/directory_tree/test_early_show_root.py index 7f85c30143..3923e2fe12 100644 --- a/tests/directory_tree/test_early_show_root.py +++ b/tests/directory_tree/test_early_show_root.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import DirectoryTree diff --git a/tests/footer/test_footer.py b/tests/footer/test_footer.py index 291b1a54c1..3b1635bf41 100644 --- a/tests/footer/test_footer.py +++ b/tests/footer/test_footer.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.binding import Binding from textual.widget import Widget diff --git a/tests/input/test_cut_copy_paste.py b/tests/input/test_cut_copy_paste.py index 3dada767f2..34e814ecfe 100644 --- a/tests/input/test_cut_copy_paste.py +++ b/tests/input/test_cut_copy_paste.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Input diff --git a/tests/input/test_input_clear.py b/tests/input/test_input_clear.py index b157dcaf5d..cf534d0b47 100644 --- a/tests/input/test_input_clear.py +++ b/tests/input/test_input_clear.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Input diff --git a/tests/input/test_input_key_modification_actions.py b/tests/input/test_input_key_modification_actions.py index 8cfc227135..f9f9d9b30c 100644 --- a/tests/input/test_input_key_modification_actions.py +++ b/tests/input/test_input_key_modification_actions.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.widgets import Input diff --git a/tests/input/test_input_key_movement_actions.py b/tests/input/test_input_key_movement_actions.py index 4c1bf72b59..5c92bf0502 100644 --- a/tests/input/test_input_key_movement_actions.py +++ b/tests/input/test_input_key_movement_actions.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.widgets import Input diff --git a/tests/input/test_input_messages.py b/tests/input/test_input_messages.py index 5acf7560f2..28c45a077c 100644 --- a/tests/input/test_input_messages.py +++ b/tests/input/test_input_messages.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual import on from textual.app import App, ComposeResult from textual.events import Paste diff --git a/tests/input/test_input_properties.py b/tests/input/test_input_properties.py index 7f249f6f81..b8e566bc47 100644 --- a/tests/input/test_input_properties.py +++ b/tests/input/test_input_properties.py @@ -1,5 +1,6 @@ from __future__ import annotations +import pytest from rich.highlighter import JSONHighlighter from rich.text import Text diff --git a/tests/input/test_input_terminal_cursor.py b/tests/input/test_input_terminal_cursor.py index 80a80fd266..982e706d21 100644 --- a/tests/input/test_input_terminal_cursor.py +++ b/tests/input/test_input_terminal_cursor.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.geometry import Offset from textual.widgets import Input diff --git a/tests/input/test_select_on_focus.py b/tests/input/test_select_on_focus.py index 7aca00c6bd..6870fb5dd8 100644 --- a/tests/input/test_select_on_focus.py +++ b/tests/input/test_select_on_focus.py @@ -1,5 +1,7 @@ """The standard path of selecting text on focus is well covered by snapshot tests.""" +import pytest + from textual import events from textual.app import App, ComposeResult from textual.widgets import Input diff --git a/tests/listview/test_inherit_listview.py b/tests/listview/test_inherit_listview.py index 59b95f15bc..1502a4338c 100644 --- a/tests/listview/test_inherit_listview.py +++ b/tests/listview/test_inherit_listview.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.binding import Binding from textual.widgets import Label, ListItem, ListView diff --git a/tests/listview/test_listview_navigation.py b/tests/listview/test_listview_navigation.py index 8384651f19..bfdc20fc1e 100644 --- a/tests/listview/test_listview_navigation.py +++ b/tests/listview/test_listview_navigation.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Label, ListItem, ListView diff --git a/tests/notifications/test_all_levels_notifications.py b/tests/notifications/test_all_levels_notifications.py index 83ba33aa27..9d79a38e3c 100644 --- a/tests/notifications/test_all_levels_notifications.py +++ b/tests/notifications/test_all_levels_notifications.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.screen import Screen from textual.widget import Widget diff --git a/tests/notifications/test_app_notifications.py b/tests/notifications/test_app_notifications.py index bb18db6527..93d78d5df5 100644 --- a/tests/notifications/test_app_notifications.py +++ b/tests/notifications/test_app_notifications.py @@ -1,5 +1,7 @@ import asyncio +import pytest + from textual.app import App diff --git a/tests/option_list/test_option_list_id_stability.py b/tests/option_list/test_option_list_id_stability.py index fe57e4c3df..7b798fa6d0 100644 --- a/tests/option_list/test_option_list_id_stability.py +++ b/tests/option_list/test_option_list_id_stability.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.widgets import OptionList from textual.widgets.option_list import Option diff --git a/tests/option_list/test_option_list_mouse_click.py b/tests/option_list/test_option_list_mouse_click.py index b080de002c..542ef0471b 100644 --- a/tests/option_list/test_option_list_mouse_click.py +++ b/tests/option_list/test_option_list_mouse_click.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual import on from textual.app import App, ComposeResult from textual.widgets import OptionList diff --git a/tests/option_list/test_option_list_mouse_hover.py b/tests/option_list/test_option_list_mouse_hover.py index 3fda38e1ef..b8a4dd6cc0 100644 --- a/tests/option_list/test_option_list_mouse_hover.py +++ b/tests/option_list/test_option_list_mouse_hover.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.geometry import Offset from textual.widgets import Label, OptionList diff --git a/tests/option_list/test_option_list_option_subclass.py b/tests/option_list/test_option_list_option_subclass.py index bf57a170ee..2d6a740664 100644 --- a/tests/option_list/test_option_list_option_subclass.py +++ b/tests/option_list/test_option_list_option_subclass.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.widgets import OptionList from textual.widgets.option_list import Option diff --git a/tests/option_list/test_option_messages.py b/tests/option_list/test_option_messages.py index e4bf45ace1..8481d2ab03 100644 --- a/tests/option_list/test_option_messages.py +++ b/tests/option_list/test_option_messages.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.geometry import Offset from textual.widgets import OptionList diff --git a/tests/select/test_changed_message.py b/tests/select/test_changed_message.py index 93f5dcf49c..873da82d84 100644 --- a/tests/select/test_changed_message.py +++ b/tests/select/test_changed_message.py @@ -1,3 +1,5 @@ +import pytest + from textual import on from textual.app import App from textual.widgets import Select diff --git a/tests/select/test_prompt.py b/tests/select/test_prompt.py index 3999739ff0..3afc2c2f5b 100644 --- a/tests/select/test_prompt.py +++ b/tests/select/test_prompt.py @@ -1,3 +1,4 @@ +import pytest from rich.text import Text from textual.app import App diff --git a/tests/select/test_remove.py b/tests/select/test_remove.py index 741a71590a..b67fbc15f1 100644 --- a/tests/select/test_remove.py +++ b/tests/select/test_remove.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Header, Select diff --git a/tests/selection_list/test_over_wide_selections.py b/tests/selection_list/test_over_wide_selections.py index 90d8af6e3a..ef21a14b6e 100644 --- a/tests/selection_list/test_over_wide_selections.py +++ b/tests/selection_list/test_over_wide_selections.py @@ -1,5 +1,7 @@ """See https://github.com/Textualize/textual/issues/2900 for the reason behind these tests.""" +import pytest + from textual.app import App, ComposeResult from textual.widgets import SelectionList diff --git a/tests/selection_list/test_selection_click_checkbox.py b/tests/selection_list/test_selection_click_checkbox.py index e967bcf52a..93f6a630a0 100644 --- a/tests/selection_list/test_selection_click_checkbox.py +++ b/tests/selection_list/test_selection_click_checkbox.py @@ -1,5 +1,7 @@ """See https://github.com/Textualize/textual/pull/2930 for the introduction of these tests.""" +import pytest + from textual import on from textual.app import App, ComposeResult from textual.geometry import Offset diff --git a/tests/selection_list/test_selection_messages.py b/tests/selection_list/test_selection_messages.py index 01edc86b09..7cbc844421 100644 --- a/tests/selection_list/test_selection_messages.py +++ b/tests/selection_list/test_selection_messages.py @@ -8,6 +8,8 @@ from __future__ import annotations +import pytest + from textual import on from textual.app import App, ComposeResult from textual.widgets import OptionList, SelectionList diff --git a/tests/selection_list/test_selection_values.py b/tests/selection_list/test_selection_values.py index ab651ba8f5..9a08e6c804 100644 --- a/tests/selection_list/test_selection_values.py +++ b/tests/selection_list/test_selection_values.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.widgets import SelectionList diff --git a/tests/test_animation.py b/tests/test_animation.py index bc19373efd..d17d7181f0 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -1,5 +1,7 @@ from time import perf_counter +import pytest + from textual.app import App, ComposeResult from textual.reactive import var from textual.widgets import Static diff --git a/tests/test_app_focus_blur.py b/tests/test_app_focus_blur.py index a78007f115..32d9507142 100644 --- a/tests/test_app_focus_blur.py +++ b/tests/test_app_focus_blur.py @@ -1,5 +1,7 @@ """Test the workings of reacting to AppFocus and AppBlur.""" +import pytest + from textual.app import App, ComposeResult from textual.events import AppBlur, AppFocus from textual.widgets import Input diff --git a/tests/test_await_remove.py b/tests/test_await_remove.py index 0feee820e3..f28acfca1d 100644 --- a/tests/test_await_remove.py +++ b/tests/test_await_remove.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.widgets import Label diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index d3f0d469f7..5610f4cedf 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -11,6 +11,8 @@ from __future__ import annotations +import pytest + from textual.actions import SkipAction from textual.app import App, ComposeResult from textual.binding import Binding diff --git a/tests/test_border_subtitle.py b/tests/test_border_subtitle.py index 585efc83b2..d430a7be7c 100644 --- a/tests/test_border_subtitle.py +++ b/tests/test_border_subtitle.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widget import Widget diff --git a/tests/test_call_x_schedulers.py b/tests/test_call_x_schedulers.py index e9c80afcb7..71f93d7a07 100644 --- a/tests/test_call_x_schedulers.py +++ b/tests/test_call_x_schedulers.py @@ -1,5 +1,7 @@ import asyncio +import pytest + from textual._context import active_message_pump from textual.app import App from textual.message_pump import MessagePump diff --git a/tests/test_collapsible.py b/tests/test_collapsible.py index fc6c0f4399..3bdb97c2ec 100644 --- a/tests/test_collapsible.py +++ b/tests/test_collapsible.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual import on from textual.app import App, ComposeResult from textual.widgets import Collapsible, Label diff --git a/tests/test_command.py b/tests/test_command.py index 61752a9e2f..cecf0a67c6 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -2,6 +2,8 @@ from typing import Iterable +import pytest + from textual.app import App, ComposeResult, SystemCommand from textual.containers import Grid from textual.screen import ModalScreen, Screen diff --git a/tests/test_compositor.py b/tests/test_compositor.py index 92dc985312..bcb1f3b7ff 100644 --- a/tests/test_compositor.py +++ b/tests/test_compositor.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.containers import Container from textual.widgets import Static diff --git a/tests/test_containers.py b/tests/test_containers.py index 6f63ce6991..b42c1670e4 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -1,5 +1,7 @@ """Test basic functioning of some containers.""" +import pytest + from textual.app import App, ComposeResult from textual.containers import ( Center, diff --git a/tests/test_demo.py b/tests/test_demo.py index 7c9a8faee3..0d2babd182 100644 --- a/tests/test_demo.py +++ b/tests/test_demo.py @@ -1,3 +1,5 @@ +import pytest + from textual.demo.demo_app import DemoApp diff --git a/tests/test_driver.py b/tests/test_driver.py index 878b0a0401..73d390c4a4 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -1,3 +1,5 @@ +import pytest + from textual import on from textual.app import App from textual.events import Click, MouseDown, MouseUp diff --git a/tests/test_dynamic_bindings.py b/tests/test_dynamic_bindings.py index 20bb2b04e6..9c6f8fee5b 100644 --- a/tests/test_dynamic_bindings.py +++ b/tests/test_dynamic_bindings.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual.app import App diff --git a/tests/test_file_monitor.py b/tests/test_file_monitor.py index a3d23558be..f13d815da0 100644 --- a/tests/test_file_monitor.py +++ b/tests/test_file_monitor.py @@ -1,6 +1,8 @@ import os from pathlib import Path +import pytest + from textual.file_monitor import FileMonitor diff --git a/tests/test_focus.py b/tests/test_focus.py index c0fb6e3439..0613acf333 100644 --- a/tests/test_focus.py +++ b/tests/test_focus.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.containers import Container, ScrollableContainer from textual.widget import Widget diff --git a/tests/test_header.py b/tests/test_header.py index 3bc5665fee..208b7a537f 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.screen import Screen from textual.widgets import Header diff --git a/tests/test_issue_4248.py b/tests/test_issue_4248.py index 518a76ef4e..e1b03d6f72 100644 --- a/tests/test_issue_4248.py +++ b/tests/test_issue_4248.py @@ -1,5 +1,7 @@ """Test https://github.com/Textualize/textual/issues/4248""" +import pytest + from textual.app import App, ComposeResult from textual.widgets import Label diff --git a/tests/test_keymap.py b/tests/test_keymap.py index e87d20edd8..4eeaefda1e 100644 --- a/tests/test_keymap.py +++ b/tests/test_keymap.py @@ -2,6 +2,8 @@ from typing import Any +import pytest + from textual.app import App, ComposeResult from textual.binding import Binding, Keymap from textual.dom import DOMNode diff --git a/tests/test_lazy.py b/tests/test_lazy.py index f74c80b7aa..b336fe1c08 100644 --- a/tests/test_lazy.py +++ b/tests/test_lazy.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.containers import Horizontal, Vertical from textual.lazy import Lazy, Reveal diff --git a/tests/test_links.py b/tests/test_links.py index 01535b14d6..8a66a9a81d 100644 --- a/tests/test_links.py +++ b/tests/test_links.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.screen import Screen from textual.widgets import Label diff --git a/tests/test_loading.py b/tests/test_loading.py index a01a875f5b..b0f618990c 100644 --- a/tests/test_loading.py +++ b/tests/test_loading.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.containers import VerticalScroll from textual.widgets import Label diff --git a/tests/test_log.py b/tests/test_log.py index 02393f2708..5b6df8915b 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Log diff --git a/tests/test_message_handling.py b/tests/test_message_handling.py index faacdd443c..e923307d80 100644 --- a/tests/test_message_handling.py +++ b/tests/test_message_handling.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.message import Message from textual.widget import Widget diff --git a/tests/test_modal.py b/tests/test_modal.py index fe3ebdd7c4..76c96ec721 100644 --- a/tests/test_modal.py +++ b/tests/test_modal.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.containers import Grid from textual.screen import ModalScreen diff --git a/tests/test_mount.py b/tests/test_mount.py index bc64bed949..8e2994e648 100644 --- a/tests/test_mount.py +++ b/tests/test_mount.py @@ -5,6 +5,8 @@ import asyncio +import pytest + from textual.app import App from textual.widget import Widget diff --git a/tests/test_overflow_change.py b/tests/test_overflow_change.py index 0d52cabd06..758ed2ded6 100644 --- a/tests/test_overflow_change.py +++ b/tests/test_overflow_change.py @@ -1,5 +1,7 @@ """Regression test for #1616 https://github.com/Textualize/textual/issues/1616""" +import pytest + from textual.app import App from textual.containers import VerticalScroll diff --git a/tests/test_paste.py b/tests/test_paste.py index 691d561069..5513457420 100644 --- a/tests/test_paste.py +++ b/tests/test_paste.py @@ -1,3 +1,5 @@ +import pytest + from textual import events from textual.app import App from textual.widgets import Input diff --git a/tests/test_shutdown.py b/tests/test_shutdown.py index 2e014e7783..61640f41ae 100644 --- a/tests/test_shutdown.py +++ b/tests/test_shutdown.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App from textual.containers import Horizontal from textual.widgets import Footer, Tree diff --git a/tests/test_style_importance.py b/tests/test_style_importance.py index ebbe2eb3d2..7d60dc11ae 100644 --- a/tests/test_style_importance.py +++ b/tests/test_style_importance.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.color import Color from textual.containers import Container diff --git a/tests/test_style_inheritance.py b/tests/test_style_inheritance.py index 35c2fbc920..75c707a773 100644 --- a/tests/test_style_inheritance.py +++ b/tests/test_style_inheritance.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Button, Static diff --git a/tests/test_switch.py b/tests/test_switch.py index 1a9b33e4e2..ccf9c69183 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Switch diff --git a/tests/test_test_runner.py b/tests/test_test_runner.py index 81f31a69f2..c31221e325 100644 --- a/tests/test_test_runner.py +++ b/tests/test_test_runner.py @@ -1,3 +1,5 @@ +import pytest + from textual import events from textual.app import App diff --git a/tests/test_tooltips.py b/tests/test_tooltips.py index 1ac448607b..15803bc372 100644 --- a/tests/test_tooltips.py +++ b/tests/test_tooltips.py @@ -1,5 +1,6 @@ """Tests for the tooltips.""" +import pytest from typing_extensions import Final from textual.app import App, ComposeResult diff --git a/tests/test_unmount.py b/tests/test_unmount.py index 72b8943bf0..47abd4235b 100644 --- a/tests/test_unmount.py +++ b/tests/test_unmount.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual import events from textual.app import App, ComposeResult from textual.containers import Container diff --git a/tests/test_visible.py b/tests/test_visible.py index f5e8f78a23..fac338765c 100644 --- a/tests/test_visible.py +++ b/tests/test_visible.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.containers import VerticalScroll from textual.widget import Widget diff --git a/tests/test_widget_removing.py b/tests/test_widget_removing.py index ff7ed7594b..b4c5d6e23c 100644 --- a/tests/test_widget_removing.py +++ b/tests/test_widget_removing.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.containers import Container, Vertical from textual.widgets import Button, Label, Static diff --git a/tests/test_widget_visibility.py b/tests/test_widget_visibility.py index 6e98140f7c..4fdc296312 100644 --- a/tests/test_widget_visibility.py +++ b/tests/test_widget_visibility.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import Label diff --git a/tests/text_area/test_escape_binding.py b/tests/text_area/test_escape_binding.py index 9374be26c0..e1fb9d9f51 100644 --- a/tests/text_area/test_escape_binding.py +++ b/tests/text_area/test_escape_binding.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.screen import ModalScreen from textual.widgets import Button, TextArea diff --git a/tests/text_area/test_messages.py b/tests/text_area/test_messages.py index 49f98b1597..d767aa3d1c 100644 --- a/tests/text_area/test_messages.py +++ b/tests/text_area/test_messages.py @@ -1,5 +1,7 @@ from typing import List +import pytest + from textual import on from textual.app import App, ComposeResult from textual.events import Event diff --git a/tests/text_area/test_textarea_cut_copy_paste.py b/tests/text_area/test_textarea_cut_copy_paste.py index 1059442f6d..2871211ada 100644 --- a/tests/text_area/test_textarea_cut_copy_paste.py +++ b/tests/text_area/test_textarea_cut_copy_paste.py @@ -1,3 +1,5 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import TextArea diff --git a/tests/toggles/test_checkbox.py b/tests/toggles/test_checkbox.py index 62bcf25630..f8aa0a287c 100644 --- a/tests/toggles/test_checkbox.py +++ b/tests/toggles/test_checkbox.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.widgets import Checkbox diff --git a/tests/toggles/test_labels.py b/tests/toggles/test_labels.py index 70207a4bed..b1c2ec6712 100644 --- a/tests/toggles/test_labels.py +++ b/tests/toggles/test_labels.py @@ -1,5 +1,7 @@ """Test that setting a toggle button's label has the desired effect.""" +import pytest + from textual.app import App, ComposeResult from textual.content import Content from textual.widgets import Checkbox, RadioButton, RadioSet diff --git a/tests/toggles/test_radiobutton.py b/tests/toggles/test_radiobutton.py index 2c67ad9cd7..16bbdfe84a 100644 --- a/tests/toggles/test_radiobutton.py +++ b/tests/toggles/test_radiobutton.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.widgets import RadioButton diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index e63038989c..dadf413445 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.widgets import RadioButton, RadioSet diff --git a/tests/tree/test_directory_tree.py b/tests/tree/test_directory_tree.py index f2ec397895..01c5ddca76 100644 --- a/tests/tree/test_directory_tree.py +++ b/tests/tree/test_directory_tree.py @@ -2,6 +2,7 @@ from pathlib import Path +import pytest from rich.text import Text from textual import on diff --git a/tests/tree/test_node_refresh.py b/tests/tree/test_node_refresh.py index a2fa7e203c..248f186ef4 100644 --- a/tests/tree/test_node_refresh.py +++ b/tests/tree/test_node_refresh.py @@ -1,3 +1,4 @@ +import pytest from rich.style import Style from rich.text import Text diff --git a/tests/tree/test_tree_availability.py b/tests/tree/test_tree_availability.py index a067326f06..d009936d94 100644 --- a/tests/tree/test_tree_availability.py +++ b/tests/tree/test_tree_availability.py @@ -2,6 +2,8 @@ from typing import Any +import pytest + from textual import on from textual.app import App, ComposeResult from textual.widgets import Tree diff --git a/tests/tree/test_tree_cursor.py b/tests/tree/test_tree_cursor.py index 58fe0d87a2..46655dd77c 100644 --- a/tests/tree/test_tree_cursor.py +++ b/tests/tree/test_tree_cursor.py @@ -2,6 +2,8 @@ from typing import Any +import pytest + from textual import on from textual.app import App, ComposeResult from textual.widgets import Tree diff --git a/tests/tree/test_tree_expand_etc.py b/tests/tree/test_tree_expand_etc.py index 093647c10b..7978b10a05 100644 --- a/tests/tree/test_tree_expand_etc.py +++ b/tests/tree/test_tree_expand_etc.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from textual.app import App, ComposeResult from textual.widgets import Tree diff --git a/tests/tree/test_tree_messages.py b/tests/tree/test_tree_messages.py index ff2e40daf1..5f64dfef00 100644 --- a/tests/tree/test_tree_messages.py +++ b/tests/tree/test_tree_messages.py @@ -2,6 +2,8 @@ from typing import Any +import pytest + from textual.app import App, ComposeResult from textual.containers import Vertical from textual.widgets import Button, Tree diff --git a/tests/workers/test_worker_manager.py b/tests/workers/test_worker_manager.py index b28d771173..c4bf2b319c 100644 --- a/tests/workers/test_worker_manager.py +++ b/tests/workers/test_worker_manager.py @@ -1,6 +1,8 @@ import asyncio import time +import pytest + from textual.app import App, ComposeResult from textual.widget import Widget from textual.worker import Worker, WorkerState From 008b03f78f4d899f6ac65daf9909b9b3e58bd3f8 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 24 Apr 2025 10:37:13 +0100 Subject: [PATCH 14/14] add anyio and poetry lock --- poetry.lock | 20 +------------------- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/poetry.lock b/poetry.lock index f2200d7a03..646a381798 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1772,24 +1772,6 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-asyncio" -version = "0.24.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, -] - -[package.dependencies] -pytest = ">=8.2,<9" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - [[package]] name = "pytest-cov" version = "5.0.0" @@ -2855,4 +2837,4 @@ syntax = ["tree-sitter", "tree-sitter-bash", "tree-sitter-css", "tree-sitter-go" [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "db29b377e8fcedd9730b54f573ba175c8743e06fa57da2dee8a4e62bc2a6faa7" +content-hash = "639f34cbb456636160acd2773e900d6df615e0f8de696cc409f4b6b0982d56cf" diff --git a/pyproject.toml b/pyproject.toml index 7d564f62c9..9b365d7314 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,6 +93,7 @@ syntax = [ ] [tool.poetry.group.dev.dependencies] +anyio = "^4.5.2" # can be upgraded to 4.9 when 3.8 support is dropped black = "24.4.2" griffe = "0.32.3" httpx = "^0.23.1"