From 1759f2c1f9a6984efa376db8a790281bf71636dd Mon Sep 17 00:00:00 2001 From: TomJGooding <101601846+TomJGooding@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:13:49 +0000 Subject: [PATCH 01/20] feat(document): add start and end properties --- src/textual/document/_document.py | 21 +++++++++++++++++++++ src/textual/widgets/_text_area.py | 5 +---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 3a4e5729b2..591e9f1c56 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -171,6 +171,16 @@ def prepare_query(self, query: str) -> Query | None: def line_count(self) -> int: """Returns the number of lines in the document.""" + @property + @abstractmethod + def start(self) -> Location: + """Returns the location of the start of the document (0, 0).""" + + @property + @abstractmethod + def end(self) -> Location: + """Returns the location of the end of the document.""" + @overload def __getitem__(self, line_index: int) -> str: ... @@ -331,6 +341,17 @@ def line_count(self) -> int: """Returns the number of lines in the document.""" return len(self._lines) + @property + def start(self) -> Location: + """Returns the location of the start of the document (0, 0).""" + return (0, 0) + + @property + def end(self) -> Location: + """Returns the location of the end of the document.""" + last_line = self._lines[-1] + return (self.line_count, len(last_line)) + def get_index_from_location(self, location: Location) -> int: """Given a location, returns the index from the document's text. diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index e6bce9aa29..b770464bc7 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2011,10 +2011,7 @@ def clear(self) -> EditResult: Returns: An EditResult relating to the deletion of all content. """ - document = self.document - last_line = document[-1] - document_end = (document.line_count, len(last_line)) - return self.delete((0, 0), document_end, maintain_selection_offset=False) + return self.delete((0, 0), self.document.end, maintain_selection_offset=False) def _delete_via_keyboard( self, From cb65d6e3b1b90c3caa3cb9a46f65d65d2debbe90 Mon Sep 17 00:00:00 2001 From: TomJGooding <101601846+TomJGooding@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:41:10 +0000 Subject: [PATCH 02/20] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b3d4909de..12e1c696a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `Pilot.resize_terminal` to resize the terminal in testing https://github.com/Textualize/textual/issues/4212 - Added `sort_children` method https://github.com/Textualize/textual/pull/4244 - Support for pseudo-classes in nested TCSS https://github.com/Textualize/textual/issues/4039 +- Added `Document.start` and `end` location properties for convenience https://github.com/Textualize/textual/pull/4267 ### Fixed From 11443e8918597b69602f3672191b3d858c7cfc46 Mon Sep 17 00:00:00 2001 From: TomJGooding <101601846+TomJGooding@users.noreply.github.com> Date: Wed, 6 Mar 2024 21:24:46 +0000 Subject: [PATCH 03/20] add tip to text area editing docs --- docs/widgets/text_area.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index c0a95c265a..9e24442d30 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -84,6 +84,10 @@ This method is the programmatic equivalent of selecting some text and then pasti Some other convenient methods are available, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear]. +!!! tip + The `TextArea.document.end` property returns the location at the end of the + document, which might be convenient when editing programmatically. + ### Working with the cursor #### Moving the cursor From 8a995fbcc131c1355a06e4d9b22c462c881407b9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 19 Mar 2024 13:39:26 +0000 Subject: [PATCH 04/20] Fix a crash in run_action when an action is an empty tuple Fixes #4248. --- src/textual/app.py | 4 +++- tests/test_issue_4248.py | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/test_issue_4248.py diff --git a/src/textual/app.py b/src/textual/app.py index aaf799a66c..b79674e713 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2926,7 +2926,9 @@ async def run_action( if isinstance(action, str): target, params = actions.parse(action) else: - target, params = action + # `action` can end up coming in as (), so if that happens we'll + # ask the action parser for a correctly-formed empty action. + target, params = action or actions.parse("") implicit_destination = True if "." in target: destination, action_name = target.split(".", 1) diff --git a/tests/test_issue_4248.py b/tests/test_issue_4248.py new file mode 100644 index 0000000000..de3eee0928 --- /dev/null +++ b/tests/test_issue_4248.py @@ -0,0 +1,46 @@ +"""Test https://github.com/Textualize/textual/issues/4248""" + +from textual.app import App, ComposeResult +from textual.widgets import Label + + +async def test_issue_4248() -> None: + """Various forms of click parameters should be fine.""" + + bumps = 0 + + class ActionApp(App[None]): + + def compose(self) -> ComposeResult: + yield Label("[@click]click me and crash[/]", id="nothing") + yield Label("[@click=]click me and crash[/]", id="no-params") + yield Label("[@click=()]click me and crash[/]", id="empty-params") + yield Label("[@click=foobar]click me[/]", id="unknown-sans-parens") + yield Label("[@click=foobar()]click me[/]", id="unknown-with-parens") + yield Label("[@click=bump]click me[/]", id="known-sans-parens") + yield Label("[@click=bump()]click me[/]", id="known-empty-parens") + yield Label("[@click=bump(100)]click me[/]", id="known-with-param") + + def action_bump(self, by_value: int = 1) -> None: + nonlocal bumps + bumps += by_value + + app = ActionApp() + async with app.run_test() as pilot: + assert bumps == 0 + await pilot.click("#nothing") + assert bumps == 0 + await pilot.click("#no-params") + assert bumps == 0 + await pilot.click("#empty-params") + assert bumps == 0 + await pilot.click("#unknown-sans-parens") + assert bumps == 0 + await pilot.click("#unknown-with-parens") + assert bumps == 0 + await pilot.click("#known-sans-parens") + assert bumps == 1 + await pilot.click("#known-empty-parens") + assert bumps == 2 + await pilot.click("#known-with-param") + assert bumps == 102 From 90c1a8b7fc55e1028122c6adcdd53ecc226a898e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 19 Mar 2024 14:23:01 +0000 Subject: [PATCH 05/20] Move responsibility for the empty action to App._broken_event --- src/textual/app.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index b79674e713..88bb5843de 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2926,9 +2926,7 @@ async def run_action( if isinstance(action, str): target, params = actions.parse(action) else: - # `action` can end up coming in as (), so if that happens we'll - # ask the action parser for a correctly-formed empty action. - target, params = action or actions.parse("") + target, params = action implicit_destination = True if "." in target: destination, action_name = target.split(".", 1) @@ -3008,11 +3006,16 @@ async def _broker_event( return False else: event.stop() - if isinstance(action, (str, tuple)): + if isinstance(action, str) or (isinstance(action, tuple) and len(action) == 2): await self.run_action(action, default_namespace=default_namespace) # type: ignore[arg-type] elif callable(action): await action() else: + if isinstance(action, tuple) and self.debug: + # It's a tuple and made it this far, which means it'll be a + # malformed action. This is a no-op, but let's log that + # anyway. + log.warning(f"{event_name} event has an empty a action!") return False return True From 564010f273a2b12644fbd0216b9fd6187df575d2 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 19 Mar 2024 14:35:18 +0000 Subject: [PATCH 06/20] Improve the log text Also make it read something akin to English. O_o --- src/textual/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index 88bb5843de..942bf8b376 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -3015,7 +3015,7 @@ async def _broker_event( # It's a tuple and made it this far, which means it'll be a # malformed action. This is a no-op, but let's log that # anyway. - log.warning(f"{event_name} event has an empty a action!") + log.warning(f"{event_name} event has an empty action") return False return True From 0907da1e2dadeeb37785640115d284bd6343de34 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 21 Mar 2024 15:33:23 +0000 Subject: [PATCH 07/20] Final form of the warning --- src/textual/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index 942bf8b376..15811232c9 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -3015,7 +3015,9 @@ async def _broker_event( # It's a tuple and made it this far, which means it'll be a # malformed action. This is a no-op, but let's log that # anyway. - log.warning(f"{event_name} event has an empty action") + log.warning( + f"Can't parse @{event_name} action from style meta; check your console markup syntax" + ) return False return True From 2b2e2a8436f8bfb509a388f393a052b1416d6271 Mon Sep 17 00:00:00 2001 From: TomJGooding <101601846+TomJGooding@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:26:18 +0000 Subject: [PATCH 08/20] add start default implementation --- src/textual/document/_document.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 591e9f1c56..ca850285b5 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -175,6 +175,7 @@ def line_count(self) -> int: @abstractmethod def start(self) -> Location: """Returns the location of the start of the document (0, 0).""" + return (0, 0) @property @abstractmethod @@ -344,7 +345,7 @@ def line_count(self) -> int: @property def start(self) -> Location: """Returns the location of the start of the document (0, 0).""" - return (0, 0) + return super().start @property def end(self) -> Location: From 667b1e81c00c6774296734ad6ab6e0ac85596355 Mon Sep 17 00:00:00 2001 From: TomJGooding <101601846+TomJGooding@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:28:19 +0000 Subject: [PATCH 09/20] update changelog --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a33d1761..7fb3ac6f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed issue with flickering scrollbars https://github.com/Textualize/textual/pull/4315 - Fix progress bar ETA not updating when setting `total` reactive https://github.com/Textualize/textual/pull/4316 -### Changed +### Changed - ProgressBar won't show ETA until there is at least one second of samples https://github.com/Textualize/textual/pull/4316 +### Added + +- Added `Document.start` and `end` location properties for convenience https://github.com/Textualize/textual/pull/4267 + ## [0.53.1] - 2023-03-18 ### Fixed @@ -31,7 +35,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `Pilot.resize_terminal` to resize the terminal in testing https://github.com/Textualize/textual/issues/4212 - Added `sort_children` method https://github.com/Textualize/textual/pull/4244 - Support for pseudo-classes in nested TCSS https://github.com/Textualize/textual/issues/4039 -- Added `Document.start` and `end` location properties for convenience https://github.com/Textualize/textual/pull/4267 ### Fixed From a80f3089dd33d2cdc45dbc91b36b788dd519a751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:15:03 +0000 Subject: [PATCH 10/20] Let pilot reraise exceptions from Widget.compose. This is probably an edge-case that wasn't covered in the original PR that introduced the machinery (namely App._exception and App._exception_event) that I want to leverage here. Related PRs: #2754 --- src/textual/widget.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index aac76e8778..4bfaabdee0 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -3618,10 +3618,8 @@ async def _compose(self) -> None: raise TypeError( f"{self!r} compose() method returned an invalid result; {error}" ) from error - except Exception: - from rich.traceback import Traceback - - self.app.panic(Traceback()) + except Exception as error: + self.app._handle_exception(error) else: self._extend_compose(widgets) await self.mount_composed_widgets(widgets) From 74ab96763fdb02583b92050ea58d897ed3d848d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:37:44 +0000 Subject: [PATCH 11/20] Add test to ensure exception reraised in tests. --- CHANGELOG.md | 1 + tests/test_pilot.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a0cb30a51..3239310763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed a crash in `TextArea` when undoing an edit to a selection the selection was made backwards https://github.com/Textualize/textual/issues/4301 - Fixed issue with flickering scrollbars https://github.com/Textualize/textual/pull/4315 - Fix progress bar ETA not updating when setting `total` reactive https://github.com/Textualize/textual/pull/4316 +- Exceptions inside `Widget.compose` weren't bubbling up in tests https://github.com/Textualize/textual/issues/4282 ### Changed diff --git a/tests/test_pilot.py b/tests/test_pilot.py index d436f8b5fc..10ea623453 100644 --- a/tests/test_pilot.py +++ b/tests/test_pilot.py @@ -7,6 +7,7 @@ from textual.binding import Binding from textual.containers import Center, Middle from textual.pilot import OutOfBounds +from textual.screen import Screen from textual.widgets import Button, Label KEY_CHARACTERS_TO_TEST = "akTW03" + punctuation @@ -56,7 +57,7 @@ def on_key(self, event: events.Key) -> None: assert keys_pressed[-1] == char -async def test_pilot_exception_catching_compose(): +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().""" @@ -70,6 +71,21 @@ def compose(self) -> ComposeResult: pass +async def test_pilot_exception_cathing_widget_compose(): + class SomeScreen(Screen[None]): + def compose(self) -> ComposeResult: + 1 / 0 + yield Label("Beep") + + class FailingApp(App[None]): + def on_mount(self) -> None: + self.push_screen(SomeScreen()) + + with pytest.raises(ZeroDivisionError): + async with FailingApp().run_test(): + pass + + 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().""" From 8c48a3b95d6b14ec20bf701a5d1e6a699be58edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:25:09 +0000 Subject: [PATCH 12/20] Surface exceptions from workers to testing frameworks. --- CHANGELOG.md | 2 +- src/textual/worker.py | 2 +- tests/test_pilot.py | 18 ++++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3239310763..ecbc5dd4b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed a crash in `TextArea` when undoing an edit to a selection the selection was made backwards https://github.com/Textualize/textual/issues/4301 - Fixed issue with flickering scrollbars https://github.com/Textualize/textual/pull/4315 - Fix progress bar ETA not updating when setting `total` reactive https://github.com/Textualize/textual/pull/4316 -- Exceptions inside `Widget.compose` weren't bubbling up in tests https://github.com/Textualize/textual/issues/4282 +- Exceptions inside `Widget.compose` or workers weren't bubbling up in tests https://github.com/Textualize/textual/issues/4282 ### Changed diff --git a/src/textual/worker.py b/src/textual/worker.py index 7719cd4dca..6cef3f6b90 100644 --- a/src/textual/worker.py +++ b/src/textual/worker.py @@ -375,7 +375,7 @@ async def _run(self, app: App) -> None: app.log.worker(Traceback()) if self.exit_on_error: - app._fatal_error() + app._handle_exception(error) else: self.state = WorkerState.SUCCESS app.log.worker(self) diff --git a/tests/test_pilot.py b/tests/test_pilot.py index 10ea623453..4a95b9c0fc 100644 --- a/tests/test_pilot.py +++ b/tests/test_pilot.py @@ -2,7 +2,7 @@ import pytest -from textual import events +from textual import events, work from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import Center, Middle @@ -71,7 +71,7 @@ def compose(self) -> ComposeResult: pass -async def test_pilot_exception_cathing_widget_compose(): +async def test_pilot_exception_catching_widget_compose(): class SomeScreen(Screen[None]): def compose(self) -> ComposeResult: 1 / 0 @@ -101,6 +101,20 @@ def action_beep(self) -> None: await pilot.press("b") +async def test_pilot_exception_catching_worker(): + class SimpleAppThatCrashes(App[None]): + def on_mount(self) -> None: + self.crash() + + @work(name="crash") + async def crash(self) -> None: + 1 / 0 + + with pytest.raises(ZeroDivisionError): + async with SimpleAppThatCrashes().run_test(): + pass + + async def test_pilot_click_screen(): """Regression test for https://github.com/Textualize/textual/issues/3395. From 4f55ca70d188d69f3c4b7aaed03d37d411043866 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 25 Mar 2024 15:43:51 +0000 Subject: [PATCH 13/20] Change Input to not suggest right away, but to wait for an edit See #3811. --- src/textual/widgets/_input.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 0d3149f2d3..0b3ae57ee1 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -519,6 +519,7 @@ def _on_focus(self, _: Focus) -> None: if self.cursor_blink: self._blink_timer.resume() self.app.cursor_position = self.cursor_screen_offset + self._suggestion = "" async def _on_key(self, event: events.Key) -> None: self._cursor_visible = True From 543881e5fb6a2222acb9eb47d6a4ee771f9dbc87 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 25 Mar 2024 15:54:35 +0000 Subject: [PATCH 14/20] Update the snapshot test --- .../__snapshots__/test_snapshots.ambr | 122 +++++++++--------- tests/snapshot_tests/test_snapshots.py | 2 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 0d054a47ee..263202293d 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -20627,137 +20627,137 @@ font-weight: 700; } - .terminal-2073605363-matrix { + .terminal-2577839347-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2073605363-title { + .terminal-2577839347-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2073605363-r1 { fill: #1e1e1e } - .terminal-2073605363-r2 { fill: #0178d4 } - .terminal-2073605363-r3 { fill: #c5c8c6 } - .terminal-2073605363-r4 { fill: #e2e2e2 } - .terminal-2073605363-r5 { fill: #1e1e1e;font-style: italic; } - .terminal-2073605363-r6 { fill: #ff0000;font-style: italic; } - .terminal-2073605363-r7 { fill: #121212 } - .terminal-2073605363-r8 { fill: #e1e1e1 } + .terminal-2577839347-r1 { fill: #1e1e1e } + .terminal-2577839347-r2 { fill: #0178d4 } + .terminal-2577839347-r3 { fill: #c5c8c6 } + .terminal-2577839347-r4 { fill: #e2e2e2 } + .terminal-2577839347-r5 { fill: #1e1e1e;font-style: italic; } + .terminal-2577839347-r6 { fill: #ff0000;font-style: italic; } + .terminal-2577839347-r7 { fill: #121212 } + .terminal-2577839347-r8 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - FruitsApp + FruitsApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - strawberry - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - straw - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - p - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - b - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - a - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + strawberry + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + straw + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + p + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + b + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + a + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index d33a4178f3..34a527d5e8 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -105,7 +105,7 @@ async def run_before(pilot): pilot.app.query(Input).first().cursor_blink = False assert snap_compare( - SNAPSHOT_APPS_DIR / "input_suggestions.py", press=[], run_before=run_before + SNAPSHOT_APPS_DIR / "input_suggestions.py", press=["b"], run_before=run_before ) From 07b6710c5d2709df126244b90fdf5e594ef0d83b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 26 Mar 2024 08:29:39 +0000 Subject: [PATCH 15/20] Remove trailing whitespace from the CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d8b9e2e2..e893fd24e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed issue where narrow TextArea would repeatedly wrap due to scrollbar appearing/disappearing https://github.com/Textualize/textual/pull/4334 - Fix progress bar ETA not updating when setting `total` reactive https://github.com/Textualize/textual/pull/4316 -### Changed +### Changed - ProgressBar won't show ETA until there is at least one second of samples https://github.com/Textualize/textual/pull/4316 From a68698df082847ca8955bd9761c7bb9a3c10cd58 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 26 Mar 2024 08:30:47 +0000 Subject: [PATCH 16/20] Update the CGANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e893fd24e4..4645bc060f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - ProgressBar won't show ETA until there is at least one second of samples https://github.com/Textualize/textual/pull/4316 +- `Input` waits until an edit has been made, after entry to the widget, before offering a suggestion https://github.com/Textualize/textual/pull/4335 ## [0.53.1] - 2023-03-18 From b8a1a5ceb80dae475c5c2e3e0f11538d20880fe8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 26 Mar 2024 10:52:28 +0000 Subject: [PATCH 17/20] version bump --- CHANGELOG.md | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4645bc060f..bdf90d941c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased +## [0.54.0] - 2023-03-26 ### Fixed @@ -1801,6 +1801,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.54.0]: https://github.com/Textualize/textual/compare/v0.53.1...v0.54.0 [0.53.1]: https://github.com/Textualize/textual/compare/v0.53.0...v0.53.1 [0.53.0]: https://github.com/Textualize/textual/compare/v0.52.1...v0.53.0 [0.52.1]: https://github.com/Textualize/textual/compare/v0.52.0...v0.52.1 diff --git a/pyproject.toml b/pyproject.toml index 5d8ebf16de..630cc27599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.53.1" +version = "0.54.0" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/" From 809f38341f35effd073d43c0625fe984f4d9e311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 26 Mar 2024 10:56:36 +0000 Subject: [PATCH 18/20] Fix tests. --- src/textual/worker.py | 3 ++- tests/test_pilot.py | 4 +++- tests/workers/test_worker.py | 18 +++++++++--------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/textual/worker.py b/src/textual/worker.py index 6cef3f6b90..f7f10b60ae 100644 --- a/src/textual/worker.py +++ b/src/textual/worker.py @@ -375,7 +375,8 @@ async def _run(self, app: App) -> None: app.log.worker(Traceback()) if self.exit_on_error: - app._handle_exception(error) + worker_failed = WorkerFailed(self._error) + app._handle_exception(worker_failed) else: self.state = WorkerState.SUCCESS app.log.worker(self) diff --git a/tests/test_pilot.py b/tests/test_pilot.py index 4a95b9c0fc..9451237fab 100644 --- a/tests/test_pilot.py +++ b/tests/test_pilot.py @@ -9,6 +9,7 @@ from textual.pilot import OutOfBounds from textual.screen import Screen from textual.widgets import Button, Label +from textual.worker import WorkerFailed KEY_CHARACTERS_TO_TEST = "akTW03" + punctuation """Test some "simple" characters (letters + digits) and all punctuation.""" @@ -110,9 +111,10 @@ def on_mount(self) -> None: async def crash(self) -> None: 1 / 0 - with pytest.raises(ZeroDivisionError): + with pytest.raises(WorkerFailed) as exc: async with SimpleAppThatCrashes().run_test(): pass + assert exc.type is ZeroDivisionError async def test_pilot_click_screen(): diff --git a/tests/workers/test_worker.py b/tests/workers/test_worker.py index 7b553d122f..61af91f638 100644 --- a/tests/workers/test_worker.py +++ b/tests/workers/test_worker.py @@ -113,10 +113,10 @@ class ErrorApp(App): pass app = ErrorApp() - async with app.run_test(): - worker: Worker[str] = Worker(app, run_error) - worker._start(app) - with pytest.raises(WorkerFailed): + with pytest.raises(WorkerFailed): + async with app.run_test(): + worker: Worker[str] = Worker(app, run_error) + worker._start(app) await worker.wait() @@ -218,12 +218,12 @@ async def self_referential_work(): await get_current_worker().wait() app = App() - async with app.run_test(): - worker = Worker(app, self_referential_work) - worker._start(app) - with pytest.raises(WorkerFailed) as exc: + with pytest.raises(WorkerFailed) as exc: + async with app.run_test(): + worker = Worker(app, self_referential_work) + worker._start(app) await worker.wait() - assert exc.type is DeadlockError + assert exc.type is DeadlockError async def test_wait_without_start(): From fe7a8998f8710ead9f228f09fd0bdddc70d141d6 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 26 Mar 2024 11:17:21 +0000 Subject: [PATCH 19/20] Add see-also relating to (un)mounting --- docs/events/load.md | 4 ++++ docs/events/mount.md | 5 +++++ docs/events/unmount.md | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/docs/events/load.md b/docs/events/load.md index 16f5e3d153..f1142f3546 100644 --- a/docs/events/load.md +++ b/docs/events/load.md @@ -1,3 +1,7 @@ ::: textual.events.Load options: heading_level: 1 + +## See also + +- [Mount](mount.md) diff --git a/docs/events/mount.md b/docs/events/mount.md index 885b8f4e9e..955391f056 100644 --- a/docs/events/mount.md +++ b/docs/events/mount.md @@ -1,3 +1,8 @@ ::: textual.events.Mount options: heading_level: 1 + +## See also + +- [Load](load.md) +- [Unmount](unmount.md) diff --git a/docs/events/unmount.md b/docs/events/unmount.md index 8e17c76924..2df6dccdd5 100644 --- a/docs/events/unmount.md +++ b/docs/events/unmount.md @@ -1,3 +1,7 @@ ::: textual.events.Unmount options: heading_level: 1 + +## See also + +- [Mount](mount.md) From fc56863f0f324e47cc9186eafa41ef2b0ea37645 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 26 Mar 2024 11:34:50 +0000 Subject: [PATCH 20/20] event docstrings --- docs/events/mouse_capture.md | 2 ++ docs/events/mouse_release.md | 2 ++ src/textual/events.py | 19 +++++++++++++------ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/events/mouse_capture.md b/docs/events/mouse_capture.md index 945da83085..953a086bf5 100644 --- a/docs/events/mouse_capture.md +++ b/docs/events/mouse_capture.md @@ -8,4 +8,6 @@ title: MouseCapture ## See also +- [capture_mouse][textual.widget.Widget.capture_mouse] +- [release_mouse][textual.widget.Widget.release_mouse] - [MouseRelease](mouse_release.md) diff --git a/docs/events/mouse_release.md b/docs/events/mouse_release.md index 438e03569b..a6e0a91cbe 100644 --- a/docs/events/mouse_release.md +++ b/docs/events/mouse_release.md @@ -8,4 +8,6 @@ title: MouseRelease ## See also +- [capture_mouse][textual.widget.Widget.capture_mouse] +- [release_mouse][textual.widget.Widget.release_mouse] - [MouseCapture](mouse_capture.md) diff --git a/src/textual/events.py b/src/textual/events.py index 7e5c158bc5..68c3ae8f3d 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -59,7 +59,7 @@ class Load(Event, bubble=False): """ Sent when the App is running but *before* the terminal is in application mode. - Use this event to run any set up that doesn't require any visuals such as loading + Use this event to run any setup that doesn't require any visuals such as loading configuration and binding keys. - [ ] Bubbles @@ -129,6 +129,9 @@ def __rich_repr__(self) -> rich.repr.Result: class Compose(Event, bubble=False, verbose=True): """Sent to a widget to request it to compose and mount children. + This event is used internally by Textual. + You won't typically need to explicitly handle it, + - [ ] Bubbles - [X] Verbose """ @@ -151,7 +154,7 @@ class Unmount(Event, bubble=False, verbose=False): class Show(Event, bubble=False): - """Sent when a widget has become visible. + """Sent when a widget is first displayed. - [ ] Bubbles - [ ] Verbose @@ -164,13 +167,17 @@ class Hide(Event, bubble=False): - [ ] Bubbles - [ ] Verbose - A widget may be hidden by setting its `visible` flag to `False`, if it is no longer in a layout, - or if it has been offset beyond the edges of the terminal. + Sent when any of the following conditions apply: + + - The widget is removed from the DOM. + - The widget is no longer displayed because it has been scrolled or clipped from the terminal or its container. + - The widget has its `display` attribute set to `False`. + - The widget's `display` style is set to `"none"`. """ class Ready(Event, bubble=False): - """Sent to the app when the DOM is ready. + """Sent to the `App` when the DOM is ready and the first frame has been displayed. - [ ] Bubbles - [ ] Verbose @@ -232,7 +239,7 @@ class Key(InputEvent): Args: key: The key that was pressed. - character: A printable character or ``None`` if it is not printable. + character: A printable character or `None` if it is not printable. """ __slots__ = ["key", "character", "aliases"]