From c66c8b6ad6223b34b31c3be26afe7028122dd569 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 09:35:01 +0000 Subject: [PATCH 01/16] Reactivity improvements --- src/textual/app.py | 4 +++ src/textual/cli/previews/easing.py | 10 +++---- src/textual/message_pump.py | 5 +++- src/textual/reactive.py | 45 ++---------------------------- src/textual/widget.py | 4 +++ src/textual/widgets/_button.py | 8 +++--- 6 files changed, 24 insertions(+), 52 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index d6a0d766d..c671cb216 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -414,6 +414,10 @@ class App(Generic[ReturnType], DOMNode): """ReturnType | None: The return type of the app.""" return self._return_value + def _post_mount(self): + """Called after the object has been mounted.""" + Reactive._initialize_object(self) + def animate( self, attribute: str, diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py index b81290302..53b7c4475 100644 --- a/src/textual/cli/previews/easing.py +++ b/src/textual/cli/previews/easing.py @@ -5,7 +5,7 @@ from textual._easing import EASING from textual.app import App, ComposeResult from textual.cli.previews.borders import TEXT from textual.containers import Container, Horizontal, Vertical -from textual.reactive import Reactive +from textual.reactive import reactive, var from textual.scrollbar import ScrollBarRender from textual.widget import Widget from textual.widgets import Button, Footer, Label, Input @@ -23,8 +23,8 @@ class EasingButtons(Widget): class Bar(Widget): - position = Reactive.init(START_POSITION) - animation_running = Reactive(False) + position = reactive(START_POSITION) + animation_running = reactive(False) DEFAULT_CSS = """ @@ -53,8 +53,8 @@ class Bar(Widget): class EasingApp(App): - position = Reactive.init(START_POSITION) - duration = Reactive.var(1.0) + position = reactive(START_POSITION) + duration = var(1.0) def on_load(self): self.bind( diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4058139ce..82fecdf5b 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -358,7 +358,10 @@ class MessagePump(metaclass=MessagePumpMeta): finally: # This is critical, mount may be waiting self._mounted_event.set() - Reactive._initialize_object(self) + self._post_mount() + + def _post_mount(self): + """Called after the object has been mounted.""" async def _process_messages_loop(self) -> None: """Process messages until the queue is closed.""" diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 93139501d..559111af4 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -7,6 +7,7 @@ from typing import ( Any, Awaitable, Callable, + ClassVar, Generic, Type, TypeVar, @@ -16,7 +17,7 @@ from typing import ( import rich.repr from . import events -from ._callback import count_parameters, invoke +from ._callback import count_parameters from ._types import MessageTarget if TYPE_CHECKING: @@ -28,15 +29,6 @@ if TYPE_CHECKING: ReactiveType = TypeVar("ReactiveType") -class _NotSet: - pass - - -_NOT_SET = _NotSet() - -T = TypeVar("T") - - @rich.repr.auto class Reactive(Generic[ReactiveType]): """Reactive descriptor. @@ -50,7 +42,7 @@ class Reactive(Generic[ReactiveType]): compute: Run compute methods when attribute is changed. Defaults to True. """ - _reactives: TypeVar[dict[str, object]] = {} + _reactives: ClassVar[dict[str, object]] = {} def __init__( self, @@ -77,37 +69,6 @@ class Reactive(Generic[ReactiveType]): yield "always_update", self._always_update yield "compute", self._run_compute - @classmethod - def init( - cls, - default: ReactiveType | Callable[[], ReactiveType], - *, - layout: bool = False, - repaint: bool = True, - always_update: bool = False, - compute: bool = True, - ) -> Reactive: - """A reactive variable that calls watchers and compute on initialize (post mount). - - Args: - default: A default value or callable that returns a default. - layout: Perform a layout on change. Defaults to False. - repaint: Perform a repaint on change. Defaults to True. - always_update: Call watchers even when the new value equals the old value. Defaults to False. - compute: Run compute methods when attribute is changed. Defaults to True. - - Returns: - A Reactive instance which calls watchers or initialize. - """ - return cls( - default, - layout=layout, - repaint=repaint, - init=True, - always_update=always_update, - compute=compute, - ) - @classmethod def var( cls, diff --git a/src/textual/widget.py b/src/textual/widget.py index 5bd204c9e..df297fcd8 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -360,6 +360,10 @@ class Widget(DOMNode): def offset(self, offset: Offset) -> None: self.styles.offset = ScalarOffset.from_offset(offset) + def _post_mount(self): + """Called after the object has been mounted.""" + Reactive._initialize_object(self) + ExpectType = TypeVar("ExpectType", bound="Widget") @overload diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index ade5a1fd8..eaa0b874b 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -11,7 +11,7 @@ from rich.text import Text, TextType from .. import events from ..css._error_tools import friendly_list from ..message import Message -from ..reactive import Reactive +from ..reactive import reactive from ..widgets import Static @@ -151,13 +151,13 @@ class Button(Static, can_focus=True): ACTIVE_EFFECT_DURATION = 0.3 """When buttons are clicked they get the `-active` class for this duration (in seconds)""" - label: Reactive[RenderableType] = Reactive("") + label: reactive[RenderableType] = reactive("") """The text label that appears within the button.""" - variant = Reactive.init("default") + variant = reactive("default") """The variant name for the button.""" - disabled = Reactive(False) + disabled = reactive(False) """The disabled state of the button; `True` if disabled, `False` if not.""" class Pressed(Message, bubble=True): From 507a2f82994910879b5aab39def46156799994e1 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 09:42:05 +0000 Subject: [PATCH 02/16] No need to return a bool here --- src/textual/reactive.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 559111af4..3d3e79de6 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -215,7 +215,7 @@ class Reactive(Generic[ReactiveType]): def invoke_watcher( watch_function: Callable, old_value: object, value: object - ) -> bool: + ) -> None: """Invoke a watch function. Args: @@ -223,8 +223,6 @@ class Reactive(Generic[ReactiveType]): old_value: The old value of the attribute. value: The new value of the attribute. - Returns: - True if the watcher was run, or False if it was posted. """ _rich_traceback_omit = True param_count = count_parameters(watch_function) @@ -241,9 +239,6 @@ class Reactive(Generic[ReactiveType]): sender=obj, callback=partial(await_watcher, watch_result) ) ) - return False - else: - return True watch_function = getattr(obj, f"watch_{name}", None) if callable(watch_function): From b0a9c743ea4ab374541db3047be663f8b7cd45d0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 09:46:07 +0000 Subject: [PATCH 03/16] Change reactable type --- src/textual/app.py | 4 ---- src/textual/dom.py | 4 ++++ src/textual/reactive.py | 6 ++---- src/textual/widget.py | 4 ---- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index c671cb216..d6a0d766d 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -414,10 +414,6 @@ class App(Generic[ReturnType], DOMNode): """ReturnType | None: The return type of the app.""" return self._return_value - def _post_mount(self): - """Called after the object has been mounted.""" - Reactive._initialize_object(self) - def animate( self, attribute: str, diff --git a/src/textual/dom.py b/src/textual/dom.py index b6129daed..c56c45b34 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -210,6 +210,10 @@ class DOMNode(MessagePump): styles = self._component_styles[name] return styles + def _post_mount(self): + """Called after the object has been mounted.""" + Reactive._initialize_object(self) + @property def _node_bases(self) -> Iterator[Type[DOMNode]]: """Iterator[Type[DOMNode]]: The DOMNode bases classes (including self.__class__)""" diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 3d3e79de6..c2863deab 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -11,7 +11,6 @@ from typing import ( Generic, Type, TypeVar, - Union, ) import rich.repr @@ -21,10 +20,9 @@ from ._callback import count_parameters from ._types import MessageTarget if TYPE_CHECKING: - from .app import App - from .widget import Widget + from .dom import DOMNode - Reactable = Union[Widget, App] + Reactable = DOMNode ReactiveType = TypeVar("ReactiveType") diff --git a/src/textual/widget.py b/src/textual/widget.py index df297fcd8..5bd204c9e 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -360,10 +360,6 @@ class Widget(DOMNode): def offset(self, offset: Offset) -> None: self.styles.offset = ScalarOffset.from_offset(offset) - def _post_mount(self): - """Called after the object has been mounted.""" - Reactive._initialize_object(self) - ExpectType = TypeVar("ExpectType", bound="Widget") @overload From 7f997023ce214d9a9b2797f2dae03f57692873e6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 10:18:01 +0000 Subject: [PATCH 04/16] force wait for idle --- src/textual/pilot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 2051c3098..b68f9409c 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -53,6 +53,7 @@ class Pilot(Generic[ReturnType]): async def wait_for_scheduled_animations(self) -> None: """Wait for any current and scheduled animations to complete.""" await self._app.animator.wait_until_complete() + await wait_for_idle(0) async def exit(self, result: ReturnType) -> None: """Exit the app with the given result. From 330e4db17c21d466d0b5246749d9459d9c79b7e2 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 9 Feb 2023 10:27:48 +0000 Subject: [PATCH 05/16] Remove the import/export of TreeNode from the widgets pyi file We've moved TreeNode out of the general widgets import, requiring the user to import from widgets.tree. When I made that change I missed this. --- src/textual/widgets/__init__.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index 82d25cd90..8fc4a8458 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -14,5 +14,4 @@ from ._static import Static as Static from ._input import Input as Input from ._text_log import TextLog as TextLog from ._tree import Tree as Tree -from ._tree_node import TreeNode as TreeNode from ._welcome import Welcome as Welcome From 6b91501ade5f6b06b09de1ff657d70ff580fc344 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 11:04:37 +0000 Subject: [PATCH 06/16] exclude removed reactables --- src/textual/message_pump.py | 5 +++++ src/textual/reactive.py | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 82fecdf5b..648b0a4d7 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -123,6 +123,11 @@ class MessagePump(metaclass=MessagePumpMeta): """ return self.app._logger + @property + def is_attached(self) -> bool: + """Check the node is attached to the DOM""" + return self._parent is not None + def _attach(self, parent: MessagePump) -> None: """Set the parent, and therefore attach this node to the tree. diff --git a/src/textual/reactive.py b/src/textual/reactive.py index c2863deab..38d631a65 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -242,9 +242,18 @@ class Reactive(Generic[ReactiveType]): if callable(watch_function): invoke_watcher(watch_function, old_value, value) - watchers: list[Callable] = getattr(obj, "__watchers", {}).get(name, []) - for watcher in watchers: - invoke_watcher(watcher, old_value, value) + # Process "global" watchers + watchers: list[tuple[Reactable, Callable]] + watchers = getattr(obj, "__watchers", {}).get(name, []) + # Remove any watchers for reactables that have since closed + if watchers: + watchers[:] = [ + (reactable, callback) + for reactable, callback in watchers + if reactable.is_attached and not reactable._closing + ] + for _, callback in watchers: + invoke_watcher(callback, old_value, value) @classmethod def _compute(cls, obj: Reactable) -> None: @@ -333,11 +342,11 @@ def watch( if not hasattr(obj, "__watchers"): setattr(obj, "__watchers", {}) - watchers: dict[str, list[Callable]] = getattr(obj, "__watchers") + watchers: dict[str, list[tuple[Reactable, Callable]]] = getattr(obj, "__watchers") watcher_list = watchers.setdefault(attribute_name, []) if callback in watcher_list: return - watcher_list.append(callback) + watcher_list.append((obj, callback)) if init: current_value = getattr(obj, attribute_name, None) Reactive._check_watchers(obj, attribute_name, current_value) From decc1e2f3cd3cec6cfa8fd60878081413eabde3e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 9 Feb 2023 11:10:30 +0000 Subject: [PATCH 07/16] Rename Checkbox to Switch A new form of Checkbox will be arriving in Textual soon, working in conjunction with a RadioButton. What was called Checkbox is perhaps a wee bit heavyweight in terms of visual design, but is a style of widget that should remain. With this in mind we're renaming the current Checkbox to Switch. In all other respects its workings remains the same, only the name has changed. Things for people to watch out for: - Imports will need to be updated. - Queries will need to be updated; special attention will need to be paid to any queries that are string-based. - CSS will need to be changed if any Checkbox styling is happening, or if any Checkbox component styles are being used. See #1725 as the initial motivation and #1746 as the issue for this particular change. --- CHANGELOG.md | 1 + docs/api/checkbox.md | 1 - docs/api/switch.md | 1 + .../widgets/{checkbox.css => switch.css} | 4 +- .../widgets/{checkbox.py => switch.py} | 20 +- docs/roadmap.md | 3 +- docs/widgets/checkbox.md | 63 ---- docs/widgets/switch.md | 63 ++++ mkdocs.yml | 4 +- src/textual/demo.css | 2 +- src/textual/demo.py | 10 +- src/textual/widgets/__init__.py | 4 +- src/textual/widgets/__init__.pyi | 2 +- .../widgets/{_checkbox.py => _switch.py} | 58 ++-- .../__snapshots__/test_snapshots.ambr | 320 +++++++++--------- tests/snapshot_tests/test_snapshots.py | 6 +- tests/test_focus.py | 12 +- 17 files changed, 288 insertions(+), 286 deletions(-) delete mode 100644 docs/api/checkbox.md create mode 100644 docs/api/switch.md rename docs/examples/widgets/{checkbox.css => switch.css} (86%) rename docs/examples/widgets/{checkbox.py => switch.py} (55%) delete mode 100644 docs/widgets/checkbox.md create mode 100644 docs/widgets/switch.md rename src/textual/widgets/{_checkbox.py => _switch.py} (65%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87edabf69..27c3fa5bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637 - `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471 +- Breaking change: renamed `Checkbox` to `Switch`. ### Fixed diff --git a/docs/api/checkbox.md b/docs/api/checkbox.md deleted file mode 100644 index 6c9c434f2..000000000 --- a/docs/api/checkbox.md +++ /dev/null @@ -1 +0,0 @@ -::: textual.widgets.Checkbox diff --git a/docs/api/switch.md b/docs/api/switch.md new file mode 100644 index 000000000..711e817a0 --- /dev/null +++ b/docs/api/switch.md @@ -0,0 +1 @@ +::: textual.widgets.Switch diff --git a/docs/examples/widgets/checkbox.css b/docs/examples/widgets/switch.css similarity index 86% rename from docs/examples/widgets/checkbox.css rename to docs/examples/widgets/switch.css index 77c9fb368..fb6a0d220 100644 --- a/docs/examples/widgets/checkbox.css +++ b/docs/examples/widgets/switch.css @@ -7,7 +7,7 @@ Screen { width: auto; } -Checkbox { +Switch { height: auto; width: auto; } @@ -22,7 +22,7 @@ Checkbox { background: darkslategrey; } -#custom-design > .checkbox--switch { +#custom-design > .switch--switch { color: dodgerblue; background: darkslateblue; } diff --git a/docs/examples/widgets/checkbox.py b/docs/examples/widgets/switch.py similarity index 55% rename from docs/examples/widgets/checkbox.py rename to docs/examples/widgets/switch.py index 400f2ae25..54a59ad63 100644 --- a/docs/examples/widgets/checkbox.py +++ b/docs/examples/widgets/switch.py @@ -1,35 +1,35 @@ from textual.app import App, ComposeResult from textual.containers import Horizontal -from textual.widgets import Checkbox, Static +from textual.widgets import Switch, Static -class CheckboxApp(App): +class SwitchApp(App): def compose(self) -> ComposeResult: - yield Static("[b]Example checkboxes\n", classes="label") + yield Static("[b]Example switches\n", classes="label") yield Horizontal( Static("off: ", classes="label"), - Checkbox(animate=False), + Switch(animate=False), classes="container", ) yield Horizontal( Static("on: ", classes="label"), - Checkbox(value=True), + Switch(value=True), classes="container", ) - focused_checkbox = Checkbox() - focused_checkbox.focus() + focused_switch = Switch() + focused_switch.focus() yield Horizontal( - Static("focused: ", classes="label"), focused_checkbox, classes="container" + Static("focused: ", classes="label"), focused_switch, classes="container" ) yield Horizontal( Static("custom: ", classes="label"), - Checkbox(id="custom-design"), + Switch(id="custom-design"), classes="container", ) -app = CheckboxApp(css_path="checkbox.css") +app = SwitchApp(css_path="switch.css") if __name__ == "__main__": app.run() diff --git a/docs/roadmap.md b/docs/roadmap.md index 7589d5c33..f486260b8 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -40,7 +40,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c - [x] Buttons * [x] Error / warning variants - [ ] Color picker -- [x] Checkbox +- [ ] Checkbox - [ ] Content switcher - [x] DataTable * [x] Cell select @@ -70,6 +70,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c * [ ] Style variants (solid, thin etc) - [ ] Radio boxes - [ ] Spark-lines +- [X] Switch - [ ] Tabs - [ ] TextArea (multi-line input) * [ ] Basic controls diff --git a/docs/widgets/checkbox.md b/docs/widgets/checkbox.md deleted file mode 100644 index a5a247ef9..000000000 --- a/docs/widgets/checkbox.md +++ /dev/null @@ -1,63 +0,0 @@ -# Checkbox - -A simple checkbox widget which stores a boolean value. - -- [x] Focusable -- [ ] Container - -## Example - -The example below shows checkboxes in various states. - -=== "Output" - - ```{.textual path="docs/examples/widgets/checkbox.py"} - ``` - -=== "checkbox.py" - - ```python - --8<-- "docs/examples/widgets/checkbox.py" - ``` - -=== "checkbox.css" - - ```sass - --8<-- "docs/examples/widgets/checkbox.css" - ``` - -## Reactive Attributes - -| Name | Type | Default | Description | -| ------- | ------ | ------- | ---------------------------------- | -| `value` | `bool` | `False` | The default value of the checkbox. | - -## Bindings - -The checkbox widget defines directly the following bindings: - -::: textual.widgets.Checkbox.BINDINGS - options: - show_root_heading: false - show_root_toc_entry: false - -## Component Classes - -The checkbox widget provides the following component classes: - -::: textual.widgets.Checkbox.COMPONENT_CLASSES - options: - show_root_heading: false - show_root_toc_entry: false - -## Messages - -### ::: textual.widgets.Checkbox.Changed - -## Additional Notes - -- To remove the spacing around a checkbox, set `border: none;` and `padding: 0;`. - -## See Also - -- [Checkbox](../api/checkbox.md) code reference diff --git a/docs/widgets/switch.md b/docs/widgets/switch.md new file mode 100644 index 000000000..1cb77be6e --- /dev/null +++ b/docs/widgets/switch.md @@ -0,0 +1,63 @@ +# Switch + +A simple switch widget which stores a boolean value. + +- [x] Focusable +- [ ] Container + +## Example + +The example below shows switches in various states. + +=== "Output" + + ```{.textual path="docs/examples/widgets/switch.py"} + ``` + +=== "switch.py" + + ```python + --8<-- "docs/examples/widgets/switch.py" + ``` + +=== "switch.css" + + ```sass + --8<-- "docs/examples/widgets/switch.css" + ``` + +## Reactive Attributes + +| Name | Type | Default | Description | +|---------|--------|---------|----------------------------------| +| `value` | `bool` | `False` | The default value of the switch. | + +## Bindings + +The switch widget defines directly the following bindings: + +::: textual.widgets.Switch.BINDINGS + options: + show_root_heading: false + show_root_toc_entry: false + +## Component Classes + +The switch widget provides the following component classes: + +::: textual.widgets.Switch.COMPONENT_CLASSES + options: + show_root_heading: false + show_root_toc_entry: false + +## Messages + +### ::: textual.widgets.Switch.Changed + +## Additional Notes + +- To remove the spacing around a `Switch`, set `border: none;` and `padding: 0;`. + +## See Also + +- [Switch](../api/switch.md) code reference diff --git a/mkdocs.yml b/mkdocs.yml index ad2d90d47..863f1288d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -126,7 +126,6 @@ nav: - "styles/width.md" - Widgets: - "widgets/button.md" - - "widgets/checkbox.md" - "widgets/data_table.md" - "widgets/directory_tree.md" - "widgets/footer.md" @@ -138,6 +137,7 @@ nav: - "widgets/list_view.md" - "widgets/placeholder.md" - "widgets/static.md" + - "widgets/switch.md" - "widgets/text_log.md" - "widgets/tree.md" - API: @@ -145,7 +145,6 @@ nav: - "api/app.md" - "api/binding.md" - "api/button.md" - - "api/checkbox.md" - "api/color.md" - "api/containers.md" - "api/coordinate.md" @@ -170,6 +169,7 @@ nav: - "api/scroll_view.md" - "api/static.md" - "api/strip.md" + - "api/switch.md" - "api/text_log.md" - "api/timer.md" - "api/tree.md" diff --git a/src/textual/demo.css b/src/textual/demo.css index c93224e9f..3fb8c7d71 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -119,7 +119,7 @@ DarkSwitch .label { color: $text-muted; } -DarkSwitch Checkbox { +DarkSwitch Switch { background: $boost; dock: left; } diff --git a/src/textual/demo.py b/src/textual/demo.py index 907d93307..9f523ab4c 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -18,7 +18,7 @@ from textual.containers import Container, Horizontal from textual.reactive import reactive, watch from textual.widgets import ( Button, - Checkbox, + Switch, DataTable, Footer, Header, @@ -138,7 +138,7 @@ Build your own or use the builtin widgets. - **Input** Text / Password input. - **Button** Clickable button with a number of styles. -- **Checkbox** A checkbox to toggle between states. +- **Switch** A switch to toggle between states. - **DataTable** A spreadsheet-like widget for navigating data. Cells may contain text or Rich renderables. - **Tree** An generic tree with expandable nodes. - **DirectoryTree** A tree of file and folders. @@ -199,16 +199,16 @@ class Title(Static): class DarkSwitch(Horizontal): def compose(self) -> ComposeResult: - yield Checkbox(value=self.app.dark) + yield Switch(value=self.app.dark) yield Static("Dark mode toggle", classes="label") def on_mount(self) -> None: watch(self.app, "dark", self.on_dark_change, init=False) def on_dark_change(self, dark: bool) -> None: - self.query_one(Checkbox).value = self.app.dark + self.query_one(Switch).value = self.app.dark - def on_checkbox_changed(self, event: Checkbox.Changed) -> None: + def on_switch_changed(self, event: Switch.Changed) -> None: self.app.dark = event.value diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 98dd81f18..e26881468 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -9,7 +9,7 @@ from ..case import camel_to_snake # be able to "see" them. if typing.TYPE_CHECKING: from ._button import Button - from ._checkbox import Checkbox + from ._switch import Switch from ._data_table import DataTable from ._directory_tree import DirectoryTree from ._footer import Footer @@ -29,7 +29,7 @@ if typing.TYPE_CHECKING: __all__ = [ "Button", - "Checkbox", + "Switch", "DataTable", "DirectoryTree", "Footer", diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index 82d25cd90..ad3b73fbb 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -1,7 +1,7 @@ # This stub file must re-export every classes exposed in the __init__.py's `__all__` list: from ._button import Button as Button from ._data_table import DataTable as DataTable -from ._checkbox import Checkbox as Checkbox +from ._switch import Switch as Switch from ._directory_tree import DirectoryTree as DirectoryTree from ._footer import Footer as Footer from ._header import Header as Header diff --git a/src/textual/widgets/_checkbox.py b/src/textual/widgets/_switch.py similarity index 65% rename from src/textual/widgets/_checkbox.py rename to src/textual/widgets/_switch.py index 9b5b1b454..cdc9f21a6 100644 --- a/src/textual/widgets/_checkbox.py +++ b/src/textual/widgets/_switch.py @@ -12,12 +12,12 @@ from ..widget import Widget from ..scrollbar import ScrollBarRender -class Checkbox(Widget, can_focus=True): - """A checkbox widget that represents a boolean value. +class Switch(Widget, can_focus=True): + """A switch widget that represents a boolean value. - Can be toggled by clicking on it or through its [bindings][textual.widgets.Checkbox.BINDINGS]. + Can be toggled by clicking on it or through its [bindings][textual.widgets.Switch.BINDINGS]. - The checkbox widget also contains [component classes][textual.widgets.Checkbox.COMPONENT_CLASSES] + The switch widget also contains [component classes][textual.widgets.Switch.COMPONENT_CLASSES] that enable more customization. """ @@ -27,20 +27,20 @@ class Checkbox(Widget, can_focus=True): """ | Key(s) | Description | | :- | :- | - | enter,space | Toggle the checkbox status. | + | enter,space | Toggle the switch state. | """ COMPONENT_CLASSES: ClassVar[set[str]] = { - "checkbox--switch", + "switch--switch", } """ | Class | Description | | :- | :- | - | `checkbox--switch` | Targets the switch of the checkbox. | + | `switch--switch` | Targets the switch of the switch. | """ DEFAULT_CSS = """ - Checkbox { + Switch { border: tall transparent; background: $panel; height: auto; @@ -48,49 +48,49 @@ class Checkbox(Widget, can_focus=True): padding: 0 2; } - Checkbox > .checkbox--switch { + Switch > .switch--switch { background: $panel-darken-2; color: $panel-lighten-2; } - Checkbox:hover { + Switch:hover { border: tall $background; } - Checkbox:focus { + Switch:focus { border: tall $accent; } - Checkbox.-on { + Switch.-on { } - Checkbox.-on > .checkbox--switch { + Switch.-on > .switch--switch { color: $success; } """ value = reactive(False, init=False) - """The value of the checkbox; `True` for on and `False` for off.""" + """The value of the switch; `True` for on and `False` for off.""" slider_pos = reactive(0.0) """The position of the slider.""" class Changed(Message, bubble=True): - """Posted when the status of the checkbox changes. + """Posted when the status of the switch changes. - Can be handled using `on_checkbox_changed` in a subclass of `Checkbox` + Can be handled using `on_switch_changed` in a subclass of `Switch` or in a parent widget in the DOM. Attributes: - value: The value that the checkbox was changed to. - input: The `Checkbox` widget that was changed. + value: The value that the switch was changed to. + input: The `Switch` widget that was changed. """ - def __init__(self, sender: Checkbox, value: bool) -> None: + def __init__(self, sender: Switch, value: bool) -> None: super().__init__(sender) self.value: bool = value - self.input: Checkbox = sender + self.input: Switch = sender def __init__( self, @@ -101,14 +101,14 @@ class Checkbox(Widget, can_focus=True): id: str | None = None, classes: str | None = None, ): - """Initialise the checkbox. + """Initialise the switch. Args: - value: The initial value of the checkbox. Defaults to False. - animate: True if the checkbox should animate when toggled. Defaults to True. - name: The name of the checkbox. - id: The ID of the checkbox in the DOM. - classes: The CSS classes of the checkbox. + value: The initial value of the switch. Defaults to False. + animate: True if the switch should animate when toggled. Defaults to True. + name: The name of the switch. + id: The ID of the switch in the DOM. + classes: The CSS classes of the switch. """ super().__init__(name=name, id=id, classes=classes) if value: @@ -128,7 +128,7 @@ class Checkbox(Widget, can_focus=True): self.set_class(slider_pos == 1, "-on") def render(self) -> RenderableType: - style = self.get_component_rich_style("checkbox--switch") + style = self.get_component_rich_style("switch--switch") return ScrollBarRender( virtual_size=100, window_size=50, @@ -150,6 +150,6 @@ class Checkbox(Widget, can_focus=True): self.toggle() def toggle(self) -> None: - """Toggle the checkbox value. As a result of the value changing, - a Checkbox.Changed message will be posted.""" + """Toggle the switch value. As a result of the value changing, + a Switch.Changed message will be posted.""" self.value = not self.value diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 5a8c647bf..0064c4c32 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -344,166 +344,6 @@ ''' # --- -# name: test_checkboxes - ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CheckboxApp - - - - - - - - - - - - - - Example checkboxes - - - ▔▔▔▔▔▔▔▔ - off:      - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - on:       - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - focused:  - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - custom:   - ▁▁▁▁▁▁▁▁ - - - - - - - - - - ''' -# --- # name: test_columns_height ''' @@ -14124,6 +13964,166 @@ ''' # --- +# name: test_switches + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SwitchApp + + + + + + + + + + + + + + Example switches + + + ▔▔▔▔▔▔▔▔ + off:      + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + on:       + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + focused:  + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + custom:   + ▁▁▁▁▁▁▁▁ + + + + + + + + + + ''' +# --- # name: test_textlog_max_lines ''' diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 9ce48d171..cdc2d0552 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -52,8 +52,8 @@ def test_dock_layout_sidebar(snap_compare): # from these examples which test rendering and simple interactions with it. -def test_checkboxes(snap_compare): - """Tests checkboxes but also acts a regression test for using +def test_switches(snap_compare): + """Tests switches but also acts a regression test for using width: auto in a Horizontal layout context.""" press = [ "shift+tab", @@ -63,7 +63,7 @@ def test_checkboxes(snap_compare): "enter", # toggle on "wait:20", ] - assert snap_compare(WIDGET_EXAMPLES_DIR / "checkbox.py", press=press) + assert snap_compare(WIDGET_EXAMPLES_DIR / "switch.py", press=press) def test_input_and_focus(snap_compare): diff --git a/tests/test_focus.py b/tests/test_focus.py index e11b14a8d..cee50c6da 100644 --- a/tests/test_focus.py +++ b/tests/test_focus.py @@ -152,21 +152,21 @@ def test_focus_next_and_previous_with_type_selector_without_self(): screen = app.screen from textual.containers import Horizontal, Vertical - from textual.widgets import Button, Checkbox, Input + from textual.widgets import Button, Switch, Input screen._add_children( Vertical( Horizontal( Input(id="w3"), - Checkbox(id="w4"), + Switch(id="w4"), Input(id="w5"), Button(id="w6"), - Checkbox(id="w7"), + Switch(id="w7"), id="w2", ), Horizontal( Button(id="w9"), - Checkbox(id="w10"), + Switch(id="w10"), Button(id="w11"), Input(id="w12"), Input(id="w13"), @@ -180,11 +180,11 @@ def test_focus_next_and_previous_with_type_selector_without_self(): assert screen.focused.id == "w3" assert screen.focus_next(Button).id == "w6" - assert screen.focus_next(Checkbox).id == "w7" + assert screen.focus_next(Switch).id == "w7" assert screen.focus_next(Input).id == "w12" assert screen.focus_previous(Button).id == "w11" - assert screen.focus_previous(Checkbox).id == "w10" + assert screen.focus_previous(Switch).id == "w10" assert screen.focus_previous(Button).id == "w9" assert screen.focus_previous(Input).id == "w5" From 93acc27482158d4c8bd75b3004ca80b39ed28dbc Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 11:17:41 +0000 Subject: [PATCH 08/16] test for is_attached --- src/textual/message_pump.py | 13 +++++++++++-- tests/test_widget_removing.py | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 648b0a4d7..ba7cc3485 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -125,8 +125,17 @@ class MessagePump(metaclass=MessagePumpMeta): @property def is_attached(self) -> bool: - """Check the node is attached to the DOM""" - return self._parent is not None + """Check the node is attached to the app via the DOM.""" + + from .app import App + + node = self + + while not isinstance(node, App): + if node._parent is None: + return False + node = node._parent + return True def _attach(self, parent: MessagePump) -> None: """Set the parent, and therefore attach this node to the tree. diff --git a/tests/test_widget_removing.py b/tests/test_widget_removing.py index e050bb09d..a33860c83 100644 --- a/tests/test_widget_removing.py +++ b/tests/test_widget_removing.py @@ -8,9 +8,13 @@ from textual.containers import Container async def test_remove_single_widget(): """It should be possible to the only widget on a screen.""" async with App().run_test() as pilot: - await pilot.app.mount(Static()) + widget = Static() + assert not widget.is_attached + await pilot.app.mount(widget) + assert widget.is_attached assert len(pilot.app.screen.children) == 1 await pilot.app.query_one(Static).remove() + assert not widget.is_attached assert len(pilot.app.screen.children) == 0 From 392b56e548567706e37e4ae1fb31be8693817028 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 11:42:58 +0000 Subject: [PATCH 09/16] Added watch method --- CHANGELOG.md | 1 + src/textual/demo.py | 4 ++-- src/textual/dom.py | 16 +++++++++++++++- src/textual/message_pump.py | 1 - src/textual/reactive.py | 6 ++++-- src/textual/widgets/_footer.py | 4 ++-- src/textual/widgets/_header.py | 6 +++--- 7 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87edabf69..96a1b1f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally - Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to Space https://github.com/Textualize/textual/issues/1433 - Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437 +- Added DOMNode.watch method https://github.com/Textualize/textual/pull/1750 ### Changed diff --git a/src/textual/demo.py b/src/textual/demo.py index 907d93307..58eb6aea0 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -15,7 +15,7 @@ from rich.text import Text from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import Container, Horizontal -from textual.reactive import reactive, watch +from textual.reactive import reactive from textual.widgets import ( Button, Checkbox, @@ -203,7 +203,7 @@ class DarkSwitch(Horizontal): yield Static("Dark mode toggle", classes="label") def on_mount(self) -> None: - watch(self.app, "dark", self.on_dark_change, init=False) + self.watch(self.app, "dark", self.on_dark_change, init=False) def on_dark_change(self, dark: bool) -> None: self.query_one(Checkbox).value = self.app.dark diff --git a/src/textual/dom.py b/src/textual/dom.py index c56c45b34..97034a56f 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -4,6 +4,7 @@ import re from inspect import getfile from typing import ( TYPE_CHECKING, + Callable, ClassVar, Iterable, Iterator, @@ -31,7 +32,7 @@ from .css.parse import parse_declarations from .css.styles import RenderStyles, Styles from .css.tokenize import IDENTIFIER from .message_pump import MessagePump -from .reactive import Reactive +from .reactive import Reactive, _watch from .timer import Timer from .walk import walk_breadth_first, walk_depth_first @@ -647,6 +648,19 @@ class DOMNode(MessagePump): """ return [child for child in self.children if child.display] + def watch( + self, obj: DOMNode, attribute_name: str, callback: Callable, init: bool = True + ) -> None: + """Watches for modifications to reactive attributes on another object. + + Args: + obj: Object containing attribute to watch. + attribute_name: Attribute to watch. + callback: A callback to run when attribute changes. + init: Check watchers on first call. + """ + _watch(self, obj, attribute_name, callback, init=init) + def get_pseudo_classes(self) -> Iterable[str]: """Get any pseudo classes applicable to this Node, e.g. hover, focus. diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index ba7cc3485..cdcc4fdcf 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -126,7 +126,6 @@ class MessagePump(metaclass=MessagePumpMeta): @property def is_attached(self) -> bool: """Check the node is attached to the app via the DOM.""" - from .app import App node = self diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 38d631a65..cd891ffec 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -325,10 +325,12 @@ class var(Reactive[ReactiveType]): ) -def watch( +def _watch( + node: DOMNode, obj: Reactable, attribute_name: str, callback: Callable[[Any], object], + *, init: bool = True, ) -> None: """Watch a reactive variable on an object. @@ -346,7 +348,7 @@ def watch( watcher_list = watchers.setdefault(attribute_name, []) if callback in watcher_list: return - watcher_list.append((obj, callback)) + watcher_list.append((node, callback)) if init: current_value = getattr(obj, attribute_name, None) Reactive._check_watchers(obj, attribute_name, current_value) diff --git a/src/textual/widgets/_footer.py b/src/textual/widgets/_footer.py index 0b1c9a808..00ccf9e1f 100644 --- a/src/textual/widgets/_footer.py +++ b/src/textual/widgets/_footer.py @@ -8,7 +8,7 @@ from rich.console import RenderableType from rich.text import Text from .. import events -from ..reactive import Reactive, watch +from ..reactive import Reactive from ..widget import Widget @@ -66,7 +66,7 @@ class Footer(Widget): self.refresh() def on_mount(self) -> None: - watch(self.screen, "focused", self._focus_changed) + self.watch(self.screen, "focused", self._focus_changed) def _focus_changed(self, focused: Widget | None) -> None: self._key_text = None diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py index b2df426cc..d09c4dad8 100644 --- a/src/textual/widgets/_header.py +++ b/src/textual/widgets/_header.py @@ -5,7 +5,7 @@ from datetime import datetime from rich.text import Text from ..widget import Widget -from ..reactive import Reactive, watch +from ..reactive import Reactive class HeaderIcon(Widget): @@ -133,5 +133,5 @@ class Header(Widget): def set_sub_title(sub_title: str) -> None: self.query_one(HeaderTitle).sub_text = sub_title - watch(self.app, "title", set_title) - watch(self.app, "sub_title", set_sub_title) + self.watch(self.app, "title", set_title) + self.watch(self.app, "sub_title", set_sub_title) From 85df8d703ea7d5f20b8c617d204b19b7a86dc197 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 11:45:19 +0000 Subject: [PATCH 10/16] typing and changelog --- CHANGELOG.md | 3 ++- src/textual/dom.py | 8 ++++++-- src/textual/reactive.py | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a1b1f62..573189437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally - Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to Space https://github.com/Textualize/textual/issues/1433 - Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437 -- Added DOMNode.watch method https://github.com/Textualize/textual/pull/1750 +- Added DOMNode.watch method https://github.com/Textualize/textual/pull/1750 ### Changed @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Removed - Methods `MessagePump.emit` and `MessagePump.emit_no_wait` https://github.com/Textualize/textual/pull/1738 +- Removed `reactive.watch` in favor of DOMNode.watch. ## [0.10.1] - 2023-01-20 diff --git a/src/textual/dom.py b/src/textual/dom.py index 97034a56f..07f994958 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -4,7 +4,6 @@ import re from inspect import getfile from typing import ( TYPE_CHECKING, - Callable, ClassVar, Iterable, Iterator, @@ -23,6 +22,7 @@ from rich.tree import Tree from ._context import NoActiveAppError from ._node_list import NodeList +from ._types import CallbackType from .binding import Bindings, BindingType from .color import BLACK, WHITE, Color from .css._error_tools import friendly_list @@ -649,7 +649,11 @@ class DOMNode(MessagePump): return [child for child in self.children if child.display] def watch( - self, obj: DOMNode, attribute_name: str, callback: Callable, init: bool = True + self, + obj: DOMNode, + attribute_name: str, + callback: CallbackType, + init: bool = True, ) -> None: """Watches for modifications to reactive attributes on another object. diff --git a/src/textual/reactive.py b/src/textual/reactive.py index cd891ffec..58f9a0837 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -17,7 +17,7 @@ import rich.repr from . import events from ._callback import count_parameters -from ._types import MessageTarget +from ._types import MessageTarget, CallbackType if TYPE_CHECKING: from .dom import DOMNode @@ -329,7 +329,7 @@ def _watch( node: DOMNode, obj: Reactable, attribute_name: str, - callback: Callable[[Any], object], + callback: CallbackType, *, init: bool = True, ) -> None: From 3a9c052d20e1a064370300e7bb2227638e46b4f7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 11:49:11 +0000 Subject: [PATCH 11/16] Added snapshot --- .../__snapshots__/test_snapshots.ambr | 158 ++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 4 + 2 files changed, 162 insertions(+) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 5a8c647bf..c196ebfd9 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -14124,6 +14124,164 @@ ''' # --- +# name: test_screen_switch + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ModalApp + + + + + + + + + + ModalApp + B + + + + + + + + + + + + + + + + + + + + + +  A  Push screen A  + + + + + ''' +# --- # name: test_textlog_max_lines ''' diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 9ce48d171..b2eef0054 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -214,3 +214,7 @@ def test_auto_width_input(snap_compare): assert snap_compare( SNAPSHOT_APPS_DIR / "auto_width_input.py", press=["tab", *"Hello"] ) + + +def test_screen_switch(snap_compare): + assert snap_compare(SNAPSHOT_APPS_DIR / "screen_switch.py", press=["a", "b"]) From 67e19d84e3c10d3bc94ad29a6996daeab3ef9ed8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 11:50:06 +0000 Subject: [PATCH 12/16] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 573189437..651949865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally - Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to Space https://github.com/Textualize/textual/issues/1433 - Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437 -- Added DOMNode.watch method https://github.com/Textualize/textual/pull/1750 +- Added DOMNode.watch and DOMNode.is_attached methods https://github.com/Textualize/textual/pull/1750 ### Changed From f450d98e3ee4f5091b13641ad82a03fa63c19a02 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 11:55:36 +0000 Subject: [PATCH 13/16] snapshot --- .../snapshot_apps/screen_switch.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/snapshot_tests/snapshot_apps/screen_switch.py diff --git a/tests/snapshot_tests/snapshot_apps/screen_switch.py b/tests/snapshot_tests/snapshot_apps/screen_switch.py new file mode 100644 index 000000000..b58866eff --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/screen_switch.py @@ -0,0 +1,38 @@ +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Static, Header, Footer + + +class ScreenA(Screen): + BINDINGS = [("b", "switch_to_b", "Switch to screen B")] + + def compose(self) -> ComposeResult: + yield Header() + yield Static("A") + yield Footer() + + def action_switch_to_b(self): + self.app.switch_screen(ScreenB()) + + +class ScreenB(Screen): + def compose(self) -> ComposeResult: + yield Header() + yield Static("B") + yield Footer() + + +class ModalApp(App): + BINDINGS = [("a", "push_a", "Push screen A")] + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + + def action_push_a(self) -> None: + self.push_screen(ScreenA()) + + +if __name__ == "__main__": + app = ModalApp() + app.run() From 74fc85054c5c4fc0b11eebe722a9b14c1b8e0b31 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 9 Feb 2023 12:07:48 +0000 Subject: [PATCH 14/16] docstring [skip ci] --- src/textual/message_pump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index cdcc4fdcf..d17bd8249 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -125,7 +125,7 @@ class MessagePump(metaclass=MessagePumpMeta): @property def is_attached(self) -> bool: - """Check the node is attached to the app via the DOM.""" + """Is the node is attached to the app via the DOM.""" from .app import App node = self From 9287f64a6690c4ccef37119aba8d5116c96e29d2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 9 Feb 2023 13:28:08 +0000 Subject: [PATCH 15/16] Add isort pre-commit hook, sort imports in src and test directories --- .pre-commit-config.yaml | 23 ++++++--- src/textual/__init__.py | 3 +- src/textual/__main__.py | 1 - src/textual/_arrange.py | 4 +- src/textual/_asyncio.py | 1 - src/textual/_border.py | 3 +- src/textual/_callback.py | 4 +- src/textual/_clock.py | 1 - src/textual/_compositor.py | 6 +-- src/textual/_context.py | 3 +- src/textual/_doc.py | 5 +- src/textual/_easing.py | 2 +- src/textual/_immutable_sequence_view.py | 3 +- src/textual/_import_app.py | 9 ++-- src/textual/_layout.py | 3 +- src/textual/_node_list.py | 2 +- src/textual/_parser.py | 12 +---- src/textual/_partition.py | 1 - src/textual/_profile.py | 2 +- src/textual/_resolve.py | 2 +- src/textual/_sleep.py | 7 ++- src/textual/_wait.py | 2 +- src/textual/_win_sleep.py | 1 - src/textual/_xterm_parser.py | 6 +-- src/textual/app.py | 1 - src/textual/case.py | 1 - src/textual/cli/cli.py | 3 +- src/textual/cli/previews/borders.py | 3 +- src/textual/cli/previews/colors.py | 2 +- src/textual/cli/previews/easing.py | 3 +- src/textual/cli/previews/keys.py | 6 +-- src/textual/cli/tools/diagnose.py | 6 ++- src/textual/css/_help_text.py | 12 ++--- src/textual/css/_style_properties.py | 16 ++---- src/textual/css/constants.py | 1 + src/textual/css/errors.py | 2 +- src/textual/css/match.py | 4 +- src/textual/css/model.py | 7 ++- src/textual/css/parse.py | 12 ++--- src/textual/css/scalar.py | 2 +- src/textual/css/scalar_animation.py | 7 +-- src/textual/css/styles.py | 3 +- src/textual/css/tokenize.py | 2 +- src/textual/css/tokenizer.py | 2 +- src/textual/css/types.py | 1 + src/textual/demo.py | 2 +- src/textual/design.py | 3 +- src/textual/devtools/client.py | 13 ++--- src/textual/devtools/redirect_output.py | 4 +- src/textual/devtools/server.py | 1 + src/textual/devtools/service.py | 4 +- src/textual/drivers/headless_driver.py | 3 +- src/textual/drivers/linux_driver.py | 13 +++-- src/textual/geometry.py | 2 +- src/textual/layouts/factory.py | 2 +- src/textual/layouts/horizontal.py | 4 +- src/textual/layouts/vertical.py | 2 +- src/textual/message.py | 6 +-- src/textual/messages.py | 4 +- src/textual/pilot.py | 6 +-- src/textual/reactive.py | 2 +- src/textual/renderables/blank.py | 2 +- src/textual/renderables/gradient.py | 3 +- src/textual/renderables/sparkline.py | 4 +- src/textual/renderables/text_opacity.py | 2 +- src/textual/renderables/tint.py | 2 +- src/textual/renderables/underline_bar.py | 3 +- src/textual/screen.py | 6 +-- src/textual/walk.py | 2 +- src/textual/widget.py | 2 +- src/textual/widgets/__init__.py | 5 +- src/textual/widgets/__init__.pyi | 6 +-- src/textual/widgets/_button.py | 3 +- src/textual/widgets/_checkbox.py | 2 +- src/textual/widgets/_data_table.py | 2 +- src/textual/widgets/_directory_tree.py | 4 +- src/textual/widgets/_header.py | 2 +- src/textual/widgets/_input.py | 2 +- src/textual/widgets/_list_view.py | 1 + src/textual/widgets/_placeholder.py | 3 +- src/textual/widgets/_pretty.py | 1 + src/textual/widgets/_welcome.py | 10 ++-- src/textual/widgets/tabs.py | 4 +- tests/css/test_help_text.py | 18 +++---- tests/css/test_parse.py | 2 +- tests/css/test_styles.py | 3 +- tests/devtools/__init__.py | 4 +- tests/devtools/test_devtools_client.py | 1 - .../test_input_key_modification_actions.py | 1 - tests/input/test_input_value_visibility.py | 1 + tests/layouts/test_content_dimensions.py | 3 +- tests/layouts/test_factory.py | 2 +- tests/listview/test_inherit_listview.py | 2 +- tests/renderables/test_sparkline.py | 27 +++++++--- tests/renderables/test_text_opacity.py | 12 +++-- tests/renderables/test_underline_bar.py | 50 +++++++------------ tests/test_animator.py | 2 +- tests/test_arrange.py | 2 +- tests/test_auto_pilot.py | 4 +- tests/test_binding.py | 2 +- tests/test_border.py | 1 - tests/test_cache.py | 3 +- tests/test_call_later.py | 1 + tests/test_color.py | 1 - tests/test_concurrency.py | 3 +- tests/test_dom.py | 2 +- tests/test_geometry.py | 3 +- tests/test_immutable_sequence_view.py | 3 +- tests/test_loop.py | 2 +- tests/test_node_list.py | 2 +- tests/test_overflow_change.py | 1 - tests/test_paste.py | 2 +- tests/test_path.py | 2 + tests/test_query.py | 2 +- tests/test_reactive.py | 2 - tests/test_screens.py | 10 ++-- tests/test_segment_tools.py | 2 +- tests/test_strip.py | 2 +- tests/test_table.py | 2 +- tests/test_test_runner.py | 2 +- tests/test_text_backend.py | 6 +++ tests/test_unmount.py | 2 +- tests/test_widget.py | 2 +- tests/test_widget_mount_point.py | 3 +- tests/test_widget_mounting.py | 4 +- tests/test_widget_removing.py | 5 +- tests/test_xterm_parser.py | 4 +- tests/tree/test_tree_get_node_by_id.py | 4 +- tests/tree/test_tree_messages.py | 3 +- tests/tree/test_tree_node_children.py | 1 + tests/tree/test_tree_node_label.py | 3 +- 131 files changed, 268 insertions(+), 297 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bee80413b..b68fd5121 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,22 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - args: ['--unsafe'] -- repo: https://github.com/psf/black - rev: 22.10.0 + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + args: [ '--unsafe' ] + - repo: https://github.com/pycqa/isort + rev: 5.12.0 hooks: - - id: black + - id: isort + name: isort (python) + language_version: '3.11' + args: ["--profile", "black", "--filter-files"] + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black exclude: ^tests/snapshot_tests diff --git a/src/textual/__init__.py b/src/textual/__init__.py index 8f10728b4..0ffb3533c 100644 --- a/src/textual/__init__.py +++ b/src/textual/__init__.py @@ -1,15 +1,14 @@ from __future__ import annotations import inspect +from typing import TYPE_CHECKING, Callable import rich.repr from rich.console import RenderableType -from typing import Callable, TYPE_CHECKING from ._context import active_app from ._log import LogGroup, LogVerbosity - if TYPE_CHECKING: from typing_extensions import TypeAlias diff --git a/src/textual/__main__.py b/src/textual/__main__.py index de8c1b79f..212d16b92 100644 --- a/src/textual/__main__.py +++ b/src/textual/__main__.py @@ -1,6 +1,5 @@ from .demo import DemoApp - if __name__ == "__main__": app = DemoApp() app.run() diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py index 2dc70d8ba..d14f0ddc4 100644 --- a/src/textual/_arrange.py +++ b/src/textual/_arrange.py @@ -3,11 +3,11 @@ from __future__ import annotations from collections import defaultdict from fractions import Fraction from operator import attrgetter -from typing import Sequence, TYPE_CHECKING +from typing import TYPE_CHECKING, Sequence -from .geometry import Region, Size, Spacing from ._layout import DockArrangeResult, WidgetPlacement from ._partition import partition +from .geometry import Region, Size, Spacing if TYPE_CHECKING: from .widget import Widget diff --git a/src/textual/_asyncio.py b/src/textual/_asyncio.py index f5c4aa5a4..ea9bf0835 100644 --- a/src/textual/_asyncio.py +++ b/src/textual/_asyncio.py @@ -5,7 +5,6 @@ Compatibility layer for asyncio. from __future__ import annotations - import sys __all__ = ["create_task"] diff --git a/src/textual/_border.py b/src/textual/_border.py index cded7790a..2da79b0ee 100644 --- a/src/textual/_border.py +++ b/src/textual/_border.py @@ -1,8 +1,7 @@ from __future__ import annotations from functools import lru_cache -from typing import cast, Tuple, Union -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple, Union, cast from rich.segment import Segment from rich.style import Style diff --git a/src/textual/_callback.py b/src/textual/_callback.py index 2759e3de4..ee3801454 100644 --- a/src/textual/_callback.py +++ b/src/textual/_callback.py @@ -2,8 +2,8 @@ from __future__ import annotations import asyncio from functools import lru_cache -from inspect import signature, isawaitable -from typing import Any, Callable, TYPE_CHECKING +from inspect import isawaitable, signature +from typing import TYPE_CHECKING, Any, Callable from . import active_app diff --git a/src/textual/_clock.py b/src/textual/_clock.py index 9867ba848..408b64f87 100644 --- a/src/textual/_clock.py +++ b/src/textual/_clock.py @@ -2,7 +2,6 @@ import asyncio from ._time import time - """ A module that serves as the single source of truth for everything time-related in a Textual app. Having this logic centralised makes it easier to simulate time in integration tests, diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index a74910d78..5489c8687 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -25,14 +25,14 @@ from rich.style import Style from . import errors from ._cells import cell_len from ._loop import loop_last -from .strip import Strip from .geometry import NULL_OFFSET, Offset, Region, Size - +from .strip import Strip if TYPE_CHECKING: - from .widget import Widget from typing_extensions import TypeAlias + from .widget import Widget + class ReflowResult(NamedTuple): """The result of a reflow operation. Describes the chances to widgets.""" diff --git a/src/textual/_context.py b/src/textual/_context.py index 625152a95..6a5562476 100644 --- a/src/textual/_context.py +++ b/src/textual/_context.py @@ -1,6 +1,5 @@ -from typing import TYPE_CHECKING - from contextvars import ContextVar +from typing import TYPE_CHECKING if TYPE_CHECKING: from .app import App diff --git a/src/textual/_doc.py b/src/textual/_doc.py index 91639d6e5..b4921ed70 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -2,14 +2,13 @@ from __future__ import annotations import hashlib import os -from pathlib import Path import shlex +from pathlib import Path from typing import Iterable +from textual._import_app import import_app from textual.app import App from textual.pilot import Pilot -from textual._import_app import import_app - SCREENSHOT_CACHE = ".screenshot_cache" diff --git a/src/textual/_easing.py b/src/textual/_easing.py index 007b643b2..13b274ddd 100644 --- a/src/textual/_easing.py +++ b/src/textual/_easing.py @@ -3,7 +3,7 @@ Define a series of easing functions for more natural-looking animations. Taken from https://easings.net/ and translated from JavaScript. """ -from math import pi, cos, sin, sqrt +from math import cos, pi, sin, sqrt def _in_out_expo(x: float) -> float: diff --git a/src/textual/_immutable_sequence_view.py b/src/textual/_immutable_sequence_view.py index fafb274e8..b9c34f0f0 100644 --- a/src/textual/_immutable_sequence_view.py +++ b/src/textual/_immutable_sequence_view.py @@ -1,8 +1,9 @@ """Provides an immutable sequence view class.""" from __future__ import annotations + from sys import maxsize -from typing import Generic, TypeVar, Iterator, overload, Sequence +from typing import Generic, Iterator, Sequence, TypeVar, overload T = TypeVar("T") diff --git a/src/textual/_import_app.py b/src/textual/_import_app.py index 4fedc4376..8176caf72 100644 --- a/src/textual/_import_app.py +++ b/src/textual/_import_app.py @@ -1,12 +1,11 @@ from __future__ import annotations import os -import sys import runpy import shlex +import sys from pathlib import Path -from typing import cast, TYPE_CHECKING - +from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from textual.app import App @@ -46,10 +45,10 @@ def import_app(import_name: str) -> App: A Textual application """ - import inspect import importlib + import inspect - from textual.app import App, WINDOWS + from textual.app import WINDOWS, App import_name, *argv = shlex.split(import_name, posix=not WINDOWS) drive, import_name = os.path.splitdrive(import_name) diff --git a/src/textual/_layout.py b/src/textual/_layout.py index 22c7129cd..5556a705e 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -1,12 +1,13 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import ClassVar, NamedTuple, TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar, NamedTuple from .geometry import Region, Size, Spacing if TYPE_CHECKING: from typing_extensions import TypeAlias + from .widget import Widget ArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget]]" diff --git a/src/textual/_node_list.py b/src/textual/_node_list.py index b8a714bc1..d5e4051cb 100644 --- a/src/textual/_node_list.py +++ b/src/textual/_node_list.py @@ -1,6 +1,6 @@ from __future__ import annotations -import sys +import sys from typing import TYPE_CHECKING, Any, Iterator, Sequence, overload import rich.repr diff --git a/src/textual/_parser.py b/src/textual/_parser.py index 5163a6014..54b0f49c6 100644 --- a/src/textual/_parser.py +++ b/src/textual/_parser.py @@ -1,16 +1,8 @@ from __future__ import annotations -from collections import deque import io -from typing import ( - Callable, - Deque, - Generator, - TypeVar, - Generic, - Union, - Iterable, -) +from collections import deque +from typing import Callable, Deque, Generator, Generic, Iterable, TypeVar, Union class ParseError(Exception): diff --git a/src/textual/_partition.py b/src/textual/_partition.py index be431af0c..858846f54 100644 --- a/src/textual/_partition.py +++ b/src/textual/_partition.py @@ -2,7 +2,6 @@ from __future__ import annotations from typing import Callable, Iterable, TypeVar - T = TypeVar("T") diff --git a/src/textual/_profile.py b/src/textual/_profile.py index a64f6402f..4a0199a6f 100644 --- a/src/textual/_profile.py +++ b/src/textual/_profile.py @@ -4,8 +4,8 @@ Timer context manager, only used in debug. """ import contextlib -from typing import Generator from time import perf_counter +from typing import Generator from . import log diff --git a/src/textual/_resolve.py b/src/textual/_resolve.py index 5a6961381..0a6671377 100644 --- a/src/textual/_resolve.py +++ b/src/textual/_resolve.py @@ -2,7 +2,7 @@ from __future__ import annotations from fractions import Fraction from itertools import accumulate -from typing import Sequence, cast, TYPE_CHECKING +from typing import TYPE_CHECKING, Sequence, cast from typing_extensions import Literal diff --git a/src/textual/_sleep.py b/src/textual/_sleep.py index 4b6176435..bdc7010dc 100644 --- a/src/textual/_sleep.py +++ b/src/textual/_sleep.py @@ -1,9 +1,8 @@ from __future__ import annotations - -from time import sleep, perf_counter -from asyncio import get_running_loop, Future -from threading import Thread, Event +from asyncio import Future, get_running_loop +from threading import Event, Thread +from time import perf_counter, sleep class Sleeper(Thread): diff --git a/src/textual/_wait.py b/src/textual/_wait.py index f7ed7d3f3..92fe8f761 100644 --- a/src/textual/_wait.py +++ b/src/textual/_wait.py @@ -1,5 +1,5 @@ from asyncio import sleep -from time import process_time, monotonic +from time import monotonic, process_time SLEEP_GRANULARITY: float = 1 / 50 SLEEP_IDLE: float = SLEEP_GRANULARITY / 2.0 diff --git a/src/textual/_win_sleep.py b/src/textual/_win_sleep.py index 16e91285d..c8dd4517a 100644 --- a/src/textual/_win_sleep.py +++ b/src/textual/_win_sleep.py @@ -7,7 +7,6 @@ This should only be imported on Windows. from time import sleep as time_sleep - __all__ = ["sleep"] diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 7111fca50..336da9af0 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -1,17 +1,15 @@ from __future__ import annotations -import unicodedata import re +import unicodedata from typing import Any, Callable, Generator, Iterable -from . import events -from . import messages +from . import events, messages from ._ansi_sequences import ANSI_SEQUENCES_KEYS from ._parser import Awaitable, Parser, TokenCallback from ._types import MessageTarget from .keys import KEY_NAME_REPLACEMENTS - # When trying to determine whether the current sequence is a supported/valid # escape sequence, at which length should we give up and consider our search # to be unsuccessful? diff --git a/src/textual/app.py b/src/textual/app.py index d6a0d766d..4398bca2b 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -50,7 +50,6 @@ from ._context import active_app from ._event_broker import NoHandler, extract_handler_actions from ._filter import LineFilter, Monochrome from ._path import _make_path_object_relative - from ._wait import wait_for_idle from .actions import SkipAction from .await_remove import AwaitRemove diff --git a/src/textual/case.py b/src/textual/case.py index 6c3414c46..e92dfa3fb 100644 --- a/src/textual/case.py +++ b/src/textual/case.py @@ -1,5 +1,4 @@ import re - from typing import Match, Pattern diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index b7048f807..a4897bf62 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -10,8 +10,8 @@ except ImportError: from importlib_metadata import version +from textual._import_app import AppFail, import_app from textual.pilot import Pilot -from textual._import_app import import_app, AppFail @click.group() @@ -26,6 +26,7 @@ def run(): def console(verbose: bool, exclude: list[str]) -> None: """Launch the textual console.""" from rich.console import Console + from textual.devtools.server import _run_devtools console = Console() diff --git a/src/textual/cli/previews/borders.py b/src/textual/cli/previews/borders.py index 5eb8d26e7..a33320a0a 100644 --- a/src/textual/cli/previews/borders.py +++ b/src/textual/cli/previews/borders.py @@ -1,8 +1,7 @@ from textual.app import App, ComposeResult from textual.constants import BORDERS -from textual.widgets import Button, Label from textual.containers import Vertical - +from textual.widgets import Button, Label TEXT = """I must not fear. Fear is the mind-killer. diff --git a/src/textual/cli/previews/colors.py b/src/textual/cli/previews/colors.py index 3ae0eb669..1cbc04f9c 100644 --- a/src/textual/cli/previews/colors.py +++ b/src/textual/cli/previews/colors.py @@ -2,7 +2,7 @@ from textual.app import App, ComposeResult from textual.containers import Horizontal, Vertical from textual.design import ColorSystem from textual.widget import Widget -from textual.widgets import Button, Footer, Static, Label +from textual.widgets import Button, Footer, Label, Static class ColorButtons(Vertical): diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py index 53b7c4475..38a0a9710 100644 --- a/src/textual/cli/previews/easing.py +++ b/src/textual/cli/previews/easing.py @@ -1,6 +1,7 @@ from __future__ import annotations from rich.console import RenderableType + from textual._easing import EASING from textual.app import App, ComposeResult from textual.cli.previews.borders import TEXT @@ -8,7 +9,7 @@ from textual.containers import Container, Horizontal, Vertical from textual.reactive import reactive, var from textual.scrollbar import ScrollBarRender from textual.widget import Widget -from textual.widgets import Button, Footer, Label, Input +from textual.widgets import Button, Footer, Input, Label VIRTUAL_SIZE = 100 WINDOW_SIZE = 10 diff --git a/src/textual/cli/previews/keys.py b/src/textual/cli/previews/keys.py index 933fac807..4be99e022 100644 --- a/src/textual/cli/previews/keys.py +++ b/src/textual/cli/previews/keys.py @@ -1,16 +1,14 @@ from __future__ import annotations from rich.panel import Panel - from rich.text import Text -from textual.app import App, ComposeResult -from textual.reactive import var, Reactive from textual import events +from textual.app import App, ComposeResult from textual.containers import Horizontal +from textual.reactive import Reactive, var from textual.widgets import Button, Header, TextLog - INSTRUCTIONS = """\ [u]Press some keys![/] diff --git a/src/textual/cli/tools/diagnose.py b/src/textual/cli/tools/diagnose.py index d1c622ad1..d6ccf0cd6 100644 --- a/src/textual/cli/tools/diagnose.py +++ b/src/textual/cli/tools/diagnose.py @@ -1,11 +1,13 @@ """Textual CLI command code to print diagnostic information.""" from __future__ import annotations + import os -import sys import platform -from typing import Any +import sys from functools import singledispatch +from typing import Any + from importlib_metadata import version from rich.console import Console, ConsoleDimensions diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py index e0f3b60ed..e873bd8a0 100644 --- a/src/textual/css/_help_text.py +++ b/src/textual/css/_help_text.py @@ -5,20 +5,20 @@ from typing import Iterable, Sequence from typing_extensions import Literal +from textual.css._error_tools import friendly_list +from textual.css.scalar import SYMBOL_UNIT + from ..color import ColorParseError -from ._help_renderables import Example, Bullet, HelpText +from ._help_renderables import Bullet, Example, HelpText from .constants import ( - VALID_BORDER, - VALID_LAYOUT, VALID_ALIGN_HORIZONTAL, VALID_ALIGN_VERTICAL, + VALID_BORDER, + VALID_LAYOUT, VALID_STYLE_FLAGS, VALID_TEXT_ALIGN, ) -from textual.css._error_tools import friendly_list -from textual.css.scalar import SYMBOL_UNIT - StylingContext = Literal["inline", "css"] """The type of styling the user was using when the error was encountered. Used to give help text specific to the context i.e. we give CSS help if the diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index f3e5908a6..f69d10f16 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -10,14 +10,7 @@ when setting and getting. from __future__ import annotations from operator import attrgetter -from typing import ( - TYPE_CHECKING, - Generic, - Iterable, - NamedTuple, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Generic, Iterable, NamedTuple, TypeVar, cast import rich.errors import rich.repr @@ -611,11 +604,8 @@ class LayoutProperty: or a ``Layout`` object. """ - from ..layouts.factory import ( - Layout, # Prevents circular import - MissingLayout, - get_layout, - ) + from ..layouts.factory import Layout # Prevents circular import + from ..layouts.factory import MissingLayout, get_layout _rich_traceback_omit = True if layout is None: diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index c3c073940..e6a7ab019 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -1,4 +1,5 @@ from __future__ import annotations + import typing from ..geometry import Spacing diff --git a/src/textual/css/errors.py b/src/textual/css/errors.py index 6908d964e..2daa7e3b3 100644 --- a/src/textual/css/errors.py +++ b/src/textual/css/errors.py @@ -1,6 +1,6 @@ from __future__ import annotations -from rich.console import ConsoleOptions, Console, RenderResult +from rich.console import Console, ConsoleOptions, RenderResult from rich.traceback import Traceback from ._help_renderables import HelpText diff --git a/src/textual/css/match.py b/src/textual/css/match.py index cc58c90b2..2f964945d 100644 --- a/src/textual/css/match.py +++ b/src/textual/css/match.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import Iterable, TYPE_CHECKING -from .model import CombinatorType, Selector, SelectorSet +from typing import TYPE_CHECKING, Iterable +from .model import CombinatorType, Selector, SelectorSet if TYPE_CHECKING: from ..dom import DOMNode diff --git a/src/textual/css/model.py b/src/textual/css/model.py index f8087ad27..cc68ee0af 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -1,11 +1,10 @@ from __future__ import annotations - -import rich.repr - from dataclasses import dataclass, field from enum import Enum -from typing import Iterable, TYPE_CHECKING +from typing import TYPE_CHECKING, Iterable + +import rich.repr from .styles import Styles from .tokenize import Token diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py index 265458fa9..fe23c084d 100644 --- a/src/textual/css/parse.py +++ b/src/textual/css/parse.py @@ -2,23 +2,23 @@ from __future__ import annotations from functools import lru_cache from pathlib import PurePath -from typing import Iterator, Iterable, NoReturn +from typing import Iterable, Iterator, NoReturn +from ..suggestions import get_suggestion +from ._styles_builder import DeclarationError, StylesBuilder from .errors import UnresolvedVariableError -from .types import Specificity3 -from ._styles_builder import StylesBuilder, DeclarationError from .model import ( + CombinatorType, Declaration, RuleSet, Selector, - CombinatorType, SelectorSet, SelectorType, ) from .styles import Styles -from ..suggestions import get_suggestion -from .tokenize import tokenize, tokenize_declarations, Token, tokenize_values +from .tokenize import Token, tokenize, tokenize_declarations, tokenize_values from .tokenizer import EOFError, ReferencedBy +from .types import Specificity3 SELECTOR_MAP: dict[str, tuple[SelectorType, Specificity3]] = { "selector": (SelectorType.TYPE, (0, 0, 1)), diff --git a/src/textual/css/scalar.py b/src/textual/css/scalar.py index 68bb5158f..d0bc4e3bc 100644 --- a/src/textual/css/scalar.py +++ b/src/textual/css/scalar.py @@ -1,9 +1,9 @@ from __future__ import annotations +import re from enum import Enum, unique from fractions import Fraction from functools import lru_cache -import re from typing import Iterable, NamedTuple import rich.repr diff --git a/src/textual/css/scalar_animation.py b/src/textual/css/scalar_animation.py index 21afd51f8..935be134a 100644 --- a/src/textual/css/scalar_animation.py +++ b/src/textual/css/scalar_animation.py @@ -2,15 +2,12 @@ from __future__ import annotations from typing import TYPE_CHECKING -from .scalar import ScalarOffset, Scalar -from .._animator import Animation -from .._animator import EasingFunction +from .._animator import Animation, EasingFunction from .._types import CallbackType - +from .scalar import Scalar, ScalarOffset if TYPE_CHECKING: from ..dom import DOMNode - from .styles import StylesBase diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 423f1889f..ba2efa93c 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -4,8 +4,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from functools import lru_cache from operator import attrgetter -from typing import Iterable, cast -from typing import TYPE_CHECKING, Any, NamedTuple +from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, cast import rich.repr from rich.style import Style diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py index 1afd78379..2a8677f68 100644 --- a/src/textual/css/tokenize.py +++ b/src/textual/css/tokenize.py @@ -4,7 +4,7 @@ import re from pathlib import PurePath from typing import Iterable -from textual.css.tokenizer import Expect, Tokenizer, Token +from textual.css.tokenizer import Expect, Token, Tokenizer PERCENT = r"-?\d+\.?\d*%" DECIMAL = r"-?\d+\.?\d*" diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py index f518d623a..3128b0a76 100644 --- a/src/textual/css/tokenizer.py +++ b/src/textual/css/tokenizer.py @@ -4,11 +4,11 @@ import re from pathlib import PurePath from typing import NamedTuple +import rich.repr from rich.console import Group, RenderableType from rich.highlighter import ReprHighlighter from rich.padding import Padding from rich.panel import Panel -import rich.repr from rich.syntax import Syntax from rich.text import Text diff --git a/src/textual/css/types.py b/src/textual/css/types.py index 60a1d2f5f..58dfd42bb 100644 --- a/src/textual/css/types.py +++ b/src/textual/css/types.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Tuple + from typing_extensions import Literal from ..color import Color diff --git a/src/textual/demo.py b/src/textual/demo.py index 58eb6aea0..6d79e6292 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -1,8 +1,8 @@ from __future__ import annotations -from importlib_metadata import version from pathlib import Path +from importlib_metadata import version from rich import box from rich.console import RenderableType from rich.json import JSON diff --git a/src/textual/design.py b/src/textual/design.py index 447f86c62..4830ac20c 100644 --- a/src/textual/design.py +++ b/src/textual/design.py @@ -7,8 +7,7 @@ from rich.padding import Padding from rich.table import Table from rich.text import Text -from .color import Color, WHITE - +from .color import WHITE, Color NUMBER_OF_SHADES = 3 diff --git a/src/textual/devtools/client.py b/src/textual/devtools/client.py index b9933feae..0ba570552 100644 --- a/src/textual/devtools/client.py +++ b/src/textual/devtools/client.py @@ -9,21 +9,14 @@ from io import StringIO from time import time from typing import Any, NamedTuple, Type +import aiohttp +import msgpack +from aiohttp import ClientConnectorError, ClientResponseError, ClientWebSocketResponse from rich.console import Console from rich.segment import Segment from .._log import LogGroup, LogVerbosity - -import aiohttp -import msgpack -from aiohttp import ( - ClientConnectorError, - ClientResponseError, - ClientWebSocketResponse, -) - - DEVTOOLS_PORT = 8081 WEBSOCKET_CONNECT_TIMEOUT = 3 LOG_QUEUE_MAXSIZE = 512 diff --git a/src/textual/devtools/redirect_output.py b/src/textual/devtools/redirect_output.py index 696b0b8ba..b79f93580 100644 --- a/src/textual/devtools/redirect_output.py +++ b/src/textual/devtools/redirect_output.py @@ -1,10 +1,10 @@ from __future__ import annotations import inspect - from typing import TYPE_CHECKING, cast -from .client import DevtoolsLog + from .._log import LogGroup, LogVerbosity +from .client import DevtoolsLog if TYPE_CHECKING: from .client import DevtoolsClient diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py index e9926aee5..4228ca6f5 100644 --- a/src/textual/devtools/server.py +++ b/src/textual/devtools/server.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio + from aiohttp.web import run_app from aiohttp.web_app import Application from aiohttp.web_request import Request diff --git a/src/textual/devtools/service.py b/src/textual/devtools/service.py index 1d35c4e90..5e85d2f89 100644 --- a/src/textual/devtools/service.py +++ b/src/textual/devtools/service.py @@ -7,19 +7,19 @@ import pickle from json import JSONDecodeError from typing import Any, cast +import msgpack from aiohttp import WSMessage, WSMsgType from aiohttp.abc import Request from aiohttp.web_ws import WebSocketResponse from rich.console import Console from rich.markup import escape -import msgpack from textual._log import LogGroup from textual._time import time from textual.devtools.renderables import ( + DevConsoleHeader, DevConsoleLog, DevConsoleNotice, - DevConsoleHeader, ) QUEUEABLE_TYPES = {"client_log", "client_spillover"} diff --git a/src/textual/drivers/headless_driver.py b/src/textual/drivers/headless_driver.py index 87a0940e7..75172b9b6 100644 --- a/src/textual/drivers/headless_driver.py +++ b/src/textual/drivers/headless_driver.py @@ -1,9 +1,10 @@ from __future__ import annotations import asyncio + +from .. import events from ..driver import Driver from ..geometry import Size -from .. import events class HeadlessDriver(Driver): diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 09260be6f..d2c00f61b 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -2,27 +2,26 @@ from __future__ import annotations import asyncio import os -from codecs import getincrementaldecoder import selectors import signal import sys import termios import tty -from typing import Any, TYPE_CHECKING +from codecs import getincrementaldecoder from threading import Event, Thread +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from rich.console import Console import rich.repr -from .. import log -from ..driver import Driver -from ..geometry import Size +from .. import events, log +from .._profile import timer from .._types import MessageTarget from .._xterm_parser import XTermParser -from .._profile import timer -from .. import events +from ..driver import Driver +from ..geometry import Size @rich.repr.auto diff --git a/src/textual/geometry.py b/src/textual/geometry.py index f2f8ef3d8..e6efbbe5d 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -9,6 +9,7 @@ from __future__ import annotations from functools import lru_cache from operator import attrgetter, itemgetter from typing import ( + TYPE_CHECKING, Any, Collection, NamedTuple, @@ -16,7 +17,6 @@ from typing import ( TypeVar, Union, cast, - TYPE_CHECKING, ) if TYPE_CHECKING: diff --git a/src/textual/layouts/factory.py b/src/textual/layouts/factory.py index 8fa724b07..7ea59dcc5 100644 --- a/src/textual/layouts/factory.py +++ b/src/textual/layouts/factory.py @@ -1,8 +1,8 @@ from __future__ import annotations from .._layout import Layout -from .horizontal import HorizontalLayout from .grid import GridLayout +from .horizontal import HorizontalLayout from .vertical import VerticalLayout LAYOUT_MAP: dict[str, type[Layout]] = { diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index 2d6736b08..7ed1820ef 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -2,9 +2,9 @@ from __future__ import annotations from fractions import Fraction -from .._resolve import resolve_box_models -from ..geometry import Size, Region from .._layout import ArrangeResult, Layout, WidgetPlacement +from .._resolve import resolve_box_models +from ..geometry import Region, Size from ..widget import Widget diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index f26e531c0..d0235092c 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -3,9 +3,9 @@ from __future__ import annotations from fractions import Fraction from typing import TYPE_CHECKING +from .._layout import ArrangeResult, Layout, WidgetPlacement from .._resolve import resolve_box_models from ..geometry import Region, Size -from .._layout import ArrangeResult, Layout, WidgetPlacement if TYPE_CHECKING: from ..widget import Widget diff --git a/src/textual/message.py b/src/textual/message.py index da0c093a7..5f46cc97a 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -1,16 +1,16 @@ from __future__ import annotations -from typing import ClassVar, TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar import rich.repr from . import _clock -from .case import camel_to_snake from ._types import MessageTarget as MessageTarget +from .case import camel_to_snake if TYPE_CHECKING: - from .widget import Widget from .message_pump import MessagePump + from .widget import Widget @rich.repr.auto diff --git a/src/textual/messages.py b/src/textual/messages.py index f23a1a8a8..fcfe2ad2c 100644 --- a/src/textual/messages.py +++ b/src/textual/messages.py @@ -1,13 +1,13 @@ from __future__ import annotations + from typing import TYPE_CHECKING import rich.repr -from .geometry import Region from ._types import CallbackType +from .geometry import Region from .message import Message - if TYPE_CHECKING: from .message_pump import MessagePump from .widget import Widget diff --git a/src/textual/pilot.py b/src/textual/pilot.py index b68f9409c..5f4ca11b1 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -1,12 +1,12 @@ from __future__ import annotations -import rich.repr - import asyncio from typing import Generic -from .app import App, ReturnType +import rich.repr + from ._wait import wait_for_idle +from .app import App, ReturnType @rich.repr.auto(angular=True) diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 58f9a0837..4c7bd1d98 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -17,7 +17,7 @@ import rich.repr from . import events from ._callback import count_parameters -from ._types import MessageTarget, CallbackType +from ._types import CallbackType, MessageTarget if TYPE_CHECKING: from .dom import DOMNode diff --git a/src/textual/renderables/blank.py b/src/textual/renderables/blank.py index 05552ac2f..80daaf5be 100644 --- a/src/textual/renderables/blank.py +++ b/src/textual/renderables/blank.py @@ -1,6 +1,6 @@ from __future__ import annotations -from rich.console import ConsoleOptions, Console, RenderResult +from rich.console import Console, ConsoleOptions, RenderResult from rich.segment import Segment from rich.style import Style diff --git a/src/textual/renderables/gradient.py b/src/textual/renderables/gradient.py index a0c5379fb..2774f6b60 100644 --- a/src/textual/renderables/gradient.py +++ b/src/textual/renderables/gradient.py @@ -1,7 +1,6 @@ from __future__ import annotations -from rich.console import ConsoleOptions, Console, RenderResult - +from rich.console import Console, ConsoleOptions, RenderResult from rich.segment import Segment from rich.style import Style diff --git a/src/textual/renderables/sparkline.py b/src/textual/renderables/sparkline.py index f2e01f409..1289029b0 100644 --- a/src/textual/renderables/sparkline.py +++ b/src/textual/renderables/sparkline.py @@ -1,10 +1,10 @@ from __future__ import annotations import statistics -from typing import Generic, Sequence, Iterable, Callable, TypeVar +from typing import Callable, Generic, Iterable, Sequence, TypeVar from rich.color import Color -from rich.console import ConsoleOptions, Console, RenderResult +from rich.console import Console, ConsoleOptions, RenderResult from rich.segment import Segment from rich.style import Style diff --git a/src/textual/renderables/text_opacity.py b/src/textual/renderables/text_opacity.py index b0c72b2a0..1ec567020 100644 --- a/src/textual/renderables/text_opacity.py +++ b/src/textual/renderables/text_opacity.py @@ -3,7 +3,7 @@ from typing import Iterable, Tuple, cast from rich.cells import cell_len from rich.color import Color -from rich.console import ConsoleOptions, Console, RenderResult, RenderableType +from rich.console import Console, ConsoleOptions, RenderableType, RenderResult from rich.segment import Segment from rich.style import Style diff --git a/src/textual/renderables/tint.py b/src/textual/renderables/tint.py index a81471fa9..d06dfed4f 100644 --- a/src/textual/renderables/tint.py +++ b/src/textual/renderables/tint.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Iterable -from rich.console import ConsoleOptions, Console, RenderResult, RenderableType +from rich.console import Console, ConsoleOptions, RenderableType, RenderResult from rich.segment import Segment from rich.style import Style diff --git a/src/textual/renderables/underline_bar.py b/src/textual/renderables/underline_bar.py index 599c7b4c5..4a7f2ea35 100644 --- a/src/textual/renderables/underline_bar.py +++ b/src/textual/renderables/underline_bar.py @@ -1,6 +1,6 @@ from __future__ import annotations -from rich.console import ConsoleOptions, Console, RenderResult +from rich.console import Console, ConsoleOptions, RenderResult from rich.style import StyleType from rich.text import Text @@ -99,6 +99,7 @@ class UnderlineBar: if __name__ == "__main__": import random from time import sleep + from rich.color import ANSI_COLOR_NAMES console = Console() diff --git a/src/textual/screen.py b/src/textual/screen.py index 3bed42f18..86ea1c4b9 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable, Iterator, TYPE_CHECKING +from typing import TYPE_CHECKING, Iterable, Iterator import rich.repr from rich.console import RenderableType @@ -9,14 +9,14 @@ from rich.style import Style from . import errors, events, messages from ._callback import invoke from ._compositor import Compositor, MapGeometry +from ._types import CallbackType from .css.match import match from .css.parse import parse_selectors from .dom import DOMNode -from .timer import Timer -from ._types import CallbackType from .geometry import Offset, Region, Size from .reactive import Reactive from .renderables.blank import Blank +from .timer import Timer from .widget import Widget if TYPE_CHECKING: diff --git a/src/textual/walk.py b/src/textual/walk.py index 654fccb67..0dc672471 100644 --- a/src/textual/walk.py +++ b/src/textual/walk.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections import deque -from typing import Iterable, Iterator, TypeVar, overload, TYPE_CHECKING +from typing import TYPE_CHECKING, Iterable, Iterator, TypeVar, overload if TYPE_CHECKING: from textual.dom import DOMNode diff --git a/src/textual/widget.py b/src/textual/widget.py index 5bd204c9e..4cb9fbbc5 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -35,9 +35,9 @@ from rich.text import Text from rich.traceback import Traceback from . import errors, events, messages -from ._asyncio import create_task from ._animator import DEFAULT_EASING, Animatable, BoundAnimator, EasingFunction from ._arrange import DockArrangeResult, arrange +from ._asyncio import create_task from ._context import active_app from ._easing import DEFAULT_SCROLL_EASING from ._layout import Layout diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 98dd81f18..13b046c03 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations -from importlib import import_module + import typing +from importlib import import_module from ..case import camel_to_snake @@ -8,6 +9,7 @@ from ..case import camel_to_snake # but also to the `__init__.pyi` file in this same folder - otherwise text editors and type checkers won't # be able to "see" them. if typing.TYPE_CHECKING: + from ..widget import Widget from ._button import Button from ._checkbox import Checkbox from ._data_table import DataTable @@ -24,7 +26,6 @@ if typing.TYPE_CHECKING: from ._text_log import TextLog from ._tree import Tree from ._welcome import Welcome - from ..widget import Widget __all__ = [ diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index 8fc4a8458..323a93d10 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -1,17 +1,17 @@ # This stub file must re-export every classes exposed in the __init__.py's `__all__` list: from ._button import Button as Button -from ._data_table import DataTable as DataTable from ._checkbox import Checkbox as Checkbox +from ._data_table import DataTable as DataTable from ._directory_tree import DirectoryTree as DirectoryTree from ._footer import Footer as Footer from ._header import Header as Header +from ._input import Input as Input from ._label import Label as Label -from ._list_view import ListView as ListView from ._list_item import ListItem as ListItem +from ._list_view import ListView as ListView from ._placeholder import Placeholder as Placeholder from ._pretty import Pretty as Pretty from ._static import Static as Static -from ._input import Input as Input from ._text_log import TextLog as TextLog from ._tree import Tree as Tree from ._welcome import Welcome as Welcome diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index eaa0b874b..fbeb60645 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -2,11 +2,11 @@ from __future__ import annotations from functools import partial from typing import cast -from typing_extensions import Literal import rich.repr from rich.console import RenderableType from rich.text import Text, TextType +from typing_extensions import Literal from .. import events from ..css._error_tools import friendly_list @@ -14,7 +14,6 @@ from ..message import Message from ..reactive import reactive from ..widgets import Static - ButtonVariant = Literal["default", "primary", "success", "warning", "error"] _VALID_BUTTON_VARIANTS = {"default", "primary", "success", "warning", "error"} diff --git a/src/textual/widgets/_checkbox.py b/src/textual/widgets/_checkbox.py index 9b5b1b454..c6f3ad9ec 100644 --- a/src/textual/widgets/_checkbox.py +++ b/src/textual/widgets/_checkbox.py @@ -8,8 +8,8 @@ from ..binding import Binding, BindingType from ..geometry import Size from ..message import Message from ..reactive import reactive -from ..widget import Widget from ..scrollbar import ScrollBarRender +from ..widget import Widget class Checkbox(Widget, can_focus=True): diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 0a641d870..4cc540c94 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass, field from itertools import chain, zip_longest from typing import Generic, Iterable, cast -from typing_extensions import ClassVar, TypeVar, Literal import rich.repr from rich.console import RenderableType @@ -12,6 +11,7 @@ from rich.protocol import is_renderable from rich.segment import Segment from rich.style import Style from rich.text import Text, TextType +from typing_extensions import ClassVar, Literal, TypeVar from .. import events, messages from .._cache import LRUCache diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 5b4845c05..9b3d33fc5 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -7,9 +7,9 @@ from typing import ClassVar from rich.style import Style from rich.text import Text, TextType -from ..message import Message -from ._tree import Tree, TreeNode, TOGGLE_STYLE from .._types import MessageTarget +from ..message import Message +from ._tree import TOGGLE_STYLE, Tree, TreeNode @dataclass diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py index d09c4dad8..12e07e4c1 100644 --- a/src/textual/widgets/_header.py +++ b/src/textual/widgets/_header.py @@ -4,8 +4,8 @@ from datetime import datetime from rich.text import Text -from ..widget import Widget from ..reactive import Reactive +from ..widget import Widget class HeaderIcon(Widget): diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index d449efee0..8528e225a 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import ClassVar import re +from typing import ClassVar from rich.cells import cell_len, get_character_cell_size from rich.console import Console, ConsoleOptions, RenderableType, RenderResult diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index 6e391d862..3d9db5892 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import ClassVar from textual.await_remove import AwaitRemove diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index d111d906b..d4bf01333 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -7,8 +7,7 @@ from typing_extensions import Literal from .. import events from ..css._error_tools import friendly_list from ..reactive import Reactive, reactive -from ..widget import Widget, RenderResult - +from ..widget import RenderResult, Widget PlaceholderVariant = Literal["default", "size", "text"] _VALID_PLACEHOLDER_VARIANTS_ORDERED: list[PlaceholderVariant] = [ diff --git a/src/textual/widgets/_pretty.py b/src/textual/widgets/_pretty.py index ff43350f5..0597886c4 100644 --- a/src/textual/widgets/_pretty.py +++ b/src/textual/widgets/_pretty.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Any + from rich.pretty import Pretty as PrettyRenderable from ..widget import Widget diff --git a/src/textual/widgets/_welcome.py b/src/textual/widgets/_welcome.py index 0ad5032dc..631437b5a 100644 --- a/src/textual/widgets/_welcome.py +++ b/src/textual/widgets/_welcome.py @@ -1,10 +1,10 @@ -from ..app import ComposeResult -from ._static import Static -from ._button import Button -from ..containers import Container - from rich.markdown import Markdown +from ..app import ComposeResult +from ..containers import Container +from ._button import Button +from ._static import Static + WELCOME_MD = """\ # Welcome! diff --git a/src/textual/widgets/tabs.py b/src/textual/widgets/tabs.py index 8dcfac98e..193cb5d45 100644 --- a/src/textual/widgets/tabs.py +++ b/src/textual/widgets/tabs.py @@ -7,11 +7,11 @@ from typing import Iterable from rich.cells import cell_len from rich.console import Console, ConsoleOptions, RenderableType, RenderResult from rich.segment import Segment -from rich.style import StyleType, Style +from rich.style import Style, StyleType from rich.text import Text from textual import events -from textual._layout_resolve import layout_resolve, Edge +from textual._layout_resolve import Edge, layout_resolve from textual.keys import Keys from textual.reactive import Reactive from textual.renderables.text_opacity import TextOpacity diff --git a/tests/css/test_help_text.py b/tests/css/test_help_text.py index 928ca2d62..e2945b057 100644 --- a/tests/css/test_help_text.py +++ b/tests/css/test_help_text.py @@ -2,17 +2,17 @@ import pytest from tests.utilities.render import render from textual.css._help_text import ( - spacing_wrong_number_of_values_help_text, - spacing_invalid_value_help_text, - scalar_help_text, - string_enum_help_text, - color_property_help_text, - border_property_help_text, - layout_property_help_text, - fractional_property_help_text, - offset_property_help_text, align_help_text, + border_property_help_text, + color_property_help_text, + fractional_property_help_text, + layout_property_help_text, + offset_property_help_text, offset_single_axis_help_text, + scalar_help_text, + spacing_invalid_value_help_text, + spacing_wrong_number_of_values_help_text, + string_enum_help_text, style_flags_property_help_text, ) diff --git a/tests/css/test_parse.py b/tests/css/test_parse.py index fed5db430..727d45af4 100644 --- a/tests/css/test_parse.py +++ b/tests/css/test_parse.py @@ -8,7 +8,7 @@ from textual.css.parse import substitute_references from textual.css.scalar import Scalar, Unit from textual.css.stylesheet import Stylesheet, StylesheetParseError from textual.css.tokenize import tokenize -from textual.css.tokenizer import Token, ReferencedBy +from textual.css.tokenizer import ReferencedBy, Token from textual.css.transition import Transition from textual.geometry import Spacing from textual.layouts.vertical import VerticalLayout diff --git a/tests/css/test_styles.py b/tests/css/test_styles.py index 8a257c0ad..e21572478 100644 --- a/tests/css/test_styles.py +++ b/tests/css/test_styles.py @@ -1,13 +1,12 @@ from decimal import Decimal import pytest - from rich.style import Style from textual.color import Color from textual.css.errors import StyleValueError from textual.css.scalar import Scalar, Unit -from textual.css.styles import Styles, RenderStyles +from textual.css.styles import RenderStyles, Styles from textual.dom import DOMNode from textual.widget import Widget diff --git a/tests/devtools/__init__.py b/tests/devtools/__init__.py index c668d5fd7..9e35d1a25 100644 --- a/tests/devtools/__init__.py +++ b/tests/devtools/__init__.py @@ -11,6 +11,4 @@ _WINDOWS = sys.platform == "win32" # and the error messages suggest the event loop is being shutdown before async fixture # teardown code has finished running. These are very rare, but are much more of an issue on # CI since they can delay builds that have passed locally. -pytestmark = pytest.mark.skipif( - _MACOS_CI or _WINDOWS, reason="Issue #411" -) +pytestmark = pytest.mark.skipif(_MACOS_CI or _WINDOWS, reason="Issue #411") diff --git a/tests/devtools/test_devtools_client.py b/tests/devtools/test_devtools_client.py index 1d7e8e8f7..88088bff0 100644 --- a/tests/devtools/test_devtools_client.py +++ b/tests/devtools/test_devtools_client.py @@ -30,7 +30,6 @@ async def test_devtools_client_is_connected(devtools): @time_machine.travel(datetime.utcfromtimestamp(TIMESTAMP)) async def test_devtools_log_places_encodes_and_queues_message(devtools): - await devtools._stop_log_queue_processing() devtools.log(DevtoolsLog("Hello, world!", CALLER)) queued_log = await devtools.log_queue.get() diff --git a/tests/input/test_input_key_modification_actions.py b/tests/input/test_input_key_modification_actions.py index 68f7e50c8..a7fc76800 100644 --- a/tests/input/test_input_key_modification_actions.py +++ b/tests/input/test_input_key_modification_actions.py @@ -5,7 +5,6 @@ from __future__ import annotations from textual.app import App, ComposeResult from textual.widgets import Input - TEST_INPUTS: dict[str | None, str] = { "empty": "", "multi-no-punctuation": "Curse your sudden but inevitable betrayal", diff --git a/tests/input/test_input_value_visibility.py b/tests/input/test_input_value_visibility.py index 9a6a25e89..e2de63502 100644 --- a/tests/input/test_input_value_visibility.py +++ b/tests/input/test_input_value_visibility.py @@ -1,4 +1,5 @@ from rich.console import Console + from textual.app import App from textual.widgets import Input diff --git a/tests/layouts/test_content_dimensions.py b/tests/layouts/test_content_dimensions.py index 1db259b4a..0811d029b 100644 --- a/tests/layouts/test_content_dimensions.py +++ b/tests/layouts/test_content_dimensions.py @@ -1,12 +1,11 @@ import pytest +from textual.geometry import Size from textual.layouts.grid import GridLayout from textual.layouts.horizontal import HorizontalLayout from textual.layouts.vertical import VerticalLayout -from textual.geometry import Size from textual.widget import Widget - LAYOUTS = [GridLayout, HorizontalLayout, VerticalLayout] diff --git a/tests/layouts/test_factory.py b/tests/layouts/test_factory.py index bece5ab6a..ab3a6667d 100644 --- a/tests/layouts/test_factory.py +++ b/tests/layouts/test_factory.py @@ -1,6 +1,6 @@ import pytest -from textual.layouts.factory import get_layout, MissingLayout +from textual.layouts.factory import MissingLayout, get_layout from textual.layouts.vertical import VerticalLayout diff --git a/tests/listview/test_inherit_listview.py b/tests/listview/test_inherit_listview.py index 2f147a28a..520b4b21c 100644 --- a/tests/listview/test_inherit_listview.py +++ b/tests/listview/test_inherit_listview.py @@ -1,5 +1,5 @@ from textual.app import App, ComposeResult -from textual.widgets import ListView, ListItem, Label +from textual.widgets import Label, ListItem, ListView class MyListView(ListView): diff --git a/tests/renderables/test_sparkline.py b/tests/renderables/test_sparkline.py index 74f1f2f6b..5285ccc6c 100644 --- a/tests/renderables/test_sparkline.py +++ b/tests/renderables/test_sparkline.py @@ -20,22 +20,35 @@ def test_sparkline_two_values_min_max(): def test_sparkline_expand_data_to_width(): - assert render(Sparkline([2, 4], - width=4)) == f"{GREEN}▁{STOP}{GREEN}▁{STOP}{RED}█{STOP}{RED}█{STOP}" + assert ( + render(Sparkline([2, 4], width=4)) + == f"{GREEN}▁{STOP}{GREEN}▁{STOP}{RED}█{STOP}{RED}█{STOP}" + ) def test_sparkline_expand_data_to_width_non_divisible(): - assert render(Sparkline([2, 4], width=3)) == f"{GREEN}▁{STOP}{GREEN}▁{STOP}{RED}█{STOP}" + assert ( + render(Sparkline([2, 4], width=3)) + == f"{GREEN}▁{STOP}{GREEN}▁{STOP}{RED}█{STOP}" + ) def test_sparkline_shrink_data_to_width(): - assert render(Sparkline([2, 2, 4, 4, 6, 6], width=3)) == f"{GREEN}▁{STOP}{BLENDED}▄{STOP}{RED}█{STOP}" + assert ( + render(Sparkline([2, 2, 4, 4, 6, 6], width=3)) + == f"{GREEN}▁{STOP}{BLENDED}▄{STOP}{RED}█{STOP}" + ) def test_sparkline_shrink_data_to_width_non_divisible(): - assert render( - Sparkline([1, 2, 3, 4, 5], width=3, summary_function=min)) == f"{GREEN}▁{STOP}{BLENDED}▄{STOP}{RED}█{STOP}" + assert ( + render(Sparkline([1, 2, 3, 4, 5], width=3, summary_function=min)) + == f"{GREEN}▁{STOP}{BLENDED}▄{STOP}{RED}█{STOP}" + ) def test_sparkline_color_blend(): - assert render(Sparkline([1, 2, 3], width=3)) == f"{GREEN}▁{STOP}{BLENDED}▄{STOP}{RED}█{STOP}" + assert ( + render(Sparkline([1, 2, 3], width=3)) + == f"{GREEN}▁{STOP}{BLENDED}▄{STOP}{RED}█{STOP}" + ) diff --git a/tests/renderables/test_text_opacity.py b/tests/renderables/test_text_opacity.py index b543667ba..877f5b459 100644 --- a/tests/renderables/test_text_opacity.py +++ b/tests/renderables/test_text_opacity.py @@ -14,7 +14,7 @@ def text(): def test_simple_text_opacity(text): blended_red_on_green = "\x1b[38;2;127;127;0;48;2;0;255;0m" - assert render(TextOpacity(text, opacity=.5)) == ( + assert render(TextOpacity(text, opacity=0.5)) == ( f"{blended_red_on_green}Hello, world!{STOP}" ) @@ -31,19 +31,21 @@ def test_text_opacity_value_of_one_noop(text): def test_ansi_colors_noop(): ansi_colored_text = Text("Hello, world!", style="red on green", end="") - assert render(TextOpacity(ansi_colored_text, opacity=.5)) == render(ansi_colored_text) + assert render(TextOpacity(ansi_colored_text, opacity=0.5)) == render( + ansi_colored_text + ) def test_text_opacity_no_style_noop(): text_no_style = Text("Hello, world!", end="") - assert render(TextOpacity(text_no_style, opacity=.2)) == render(text_no_style) + assert render(TextOpacity(text_no_style, opacity=0.2)) == render(text_no_style) def test_text_opacity_only_fg_noop(): text_only_fg = Text("Hello, world!", style="#ff0000", end="") - assert render(TextOpacity(text_only_fg, opacity=.5)) == render(text_only_fg) + assert render(TextOpacity(text_only_fg, opacity=0.5)) == render(text_only_fg) def test_text_opacity_only_bg_noop(): text_only_bg = Text("Hello, world!", style="on #ff0000", end="") - assert render(TextOpacity(text_only_bg, opacity=.5)) == render(text_only_bg) + assert render(TextOpacity(text_only_bg, opacity=0.5)) == render(text_only_bg) diff --git a/tests/renderables/test_underline_bar.py b/tests/renderables/test_underline_bar.py index 549b331e3..b195d6f64 100644 --- a/tests/renderables/test_underline_bar.py +++ b/tests/renderables/test_underline_bar.py @@ -1,7 +1,6 @@ from unittest.mock import create_autospec -from rich.console import Console -from rich.console import ConsoleOptions +from rich.console import Console, ConsoleOptions from rich.text import Text from tests.utilities.render import render @@ -21,16 +20,12 @@ def test_no_highlight(): def test_highlight_from_zero(): bar = UnderlineBar(highlight_range=(0, 2.5), width=6) - assert render(bar) == ( - f"{MAGENTA}━━{STOP}{MAGENTA}╸{STOP}{GREY}━━━{STOP}" - ) + assert render(bar) == (f"{MAGENTA}━━{STOP}{MAGENTA}╸{STOP}{GREY}━━━{STOP}") def test_highlight_from_zero_point_five(): bar = UnderlineBar(highlight_range=(0.5, 2), width=6) - assert render(bar) == ( - f"{MAGENTA}╺━{STOP}{GREY}╺{STOP}{GREY}━━━{STOP}" - ) + assert render(bar) == (f"{MAGENTA}╺━{STOP}{GREY}╺{STOP}{GREY}━━━{STOP}") def test_highlight_middle(): @@ -47,10 +42,7 @@ def test_highlight_middle(): def test_highlight_half_start(): bar = UnderlineBar(highlight_range=(2.5, 4), width=6) assert render(bar) == ( - f"{GREY}━━{STOP}" - f"{MAGENTA}╺━{STOP}" - f"{GREY}╺{STOP}" - f"{GREY}━{STOP}" + f"{GREY}━━{STOP}" f"{MAGENTA}╺━{STOP}" f"{GREY}╺{STOP}" f"{GREY}━{STOP}" ) @@ -68,42 +60,30 @@ def test_highlight_half_end(): def test_highlight_half_start_and_half_end(): bar = UnderlineBar(highlight_range=(2.5, 4.5), width=6) assert render(bar) == ( - f"{GREY}━━{STOP}" - f"{MAGENTA}╺━{STOP}" - f"{MAGENTA}╸{STOP}" - f"{GREY}━{STOP}" + f"{GREY}━━{STOP}" f"{MAGENTA}╺━{STOP}" f"{MAGENTA}╸{STOP}" f"{GREY}━{STOP}" ) def test_highlight_to_near_end(): bar = UnderlineBar(highlight_range=(3, 5.5), width=6) assert render(bar) == ( - f"{GREY}━━{STOP}" - f"{GREY}╸{STOP}" - f"{MAGENTA}━━{STOP}" - f"{MAGENTA}╸{STOP}" + f"{GREY}━━{STOP}" f"{GREY}╸{STOP}" f"{MAGENTA}━━{STOP}" f"{MAGENTA}╸{STOP}" ) def test_highlight_to_end(): bar = UnderlineBar(highlight_range=(3, 6), width=6) - assert render(bar) == ( - f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}" - ) + assert render(bar) == (f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}") def test_highlight_out_of_bounds_start(): bar = UnderlineBar(highlight_range=(-2, 3), width=6) - assert render(bar) == ( - f"{MAGENTA}━━━{STOP}{GREY}╺{STOP}{GREY}━━{STOP}" - ) + assert render(bar) == (f"{MAGENTA}━━━{STOP}{GREY}╺{STOP}{GREY}━━{STOP}") def test_highlight_out_of_bounds_end(): bar = UnderlineBar(highlight_range=(3, 9), width=6) - assert render(bar) == ( - f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}" - ) + assert render(bar) == (f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}") def test_highlight_full_range_out_of_bounds_end(): @@ -117,7 +97,9 @@ def test_highlight_full_range_out_of_bounds_start(): def test_custom_styles(): - bar = UnderlineBar(highlight_range=(2, 4), highlight_style="red", background_style="green", width=6) + bar = UnderlineBar( + highlight_range=(2, 4), highlight_style="red", background_style="green", width=6 + ) assert render(bar) == ( f"{GREEN}━{STOP}" f"{GREEN}╸{STOP}" @@ -128,7 +110,9 @@ def test_custom_styles(): def test_clickable_ranges(): - bar = UnderlineBar(highlight_range=(0, 1), width=6, clickable_ranges={"foo": (0, 2), "bar": (4, 5)}) + bar = UnderlineBar( + highlight_range=(0, 1), width=6, clickable_ranges={"foo": (0, 2), "bar": (4, 5)} + ) console = create_autospec(Console) options = create_autospec(ConsoleOptions) @@ -136,8 +120,8 @@ def test_clickable_ranges(): start, end, style = text.spans[-2] assert (start, end) == (0, 2) - assert style.meta == {'@click': "range_clicked('foo')"} + assert style.meta == {"@click": "range_clicked('foo')"} start, end, style = text.spans[-1] assert (start, end) == (4, 5) - assert style.meta == {'@click': "range_clicked('bar')"} + assert style.meta == {"@click": "range_clicked('bar')"} diff --git a/tests/test_animator.py b/tests/test_animator.py index a92f38c96..e5be30ab6 100644 --- a/tests/test_animator.py +++ b/tests/test_animator.py @@ -6,7 +6,7 @@ from unittest.mock import Mock import pytest from textual._animator import Animator, SimpleAnimation -from textual._easing import EASING, DEFAULT_EASING +from textual._easing import DEFAULT_EASING, EASING class Animatable: diff --git a/tests/test_arrange.py b/tests/test_arrange.py index ad70e5cbc..31e030b1b 100644 --- a/tests/test_arrange.py +++ b/tests/test_arrange.py @@ -1,6 +1,6 @@ import pytest -from textual._arrange import arrange, TOP_Z +from textual._arrange import TOP_Z, arrange from textual._layout import WidgetPlacement from textual.geometry import Region, Size, Spacing from textual.widget import Widget diff --git a/tests/test_auto_pilot.py b/tests/test_auto_pilot.py index 5e2834c83..a6d4c2d1a 100644 --- a/tests/test_auto_pilot.py +++ b/tests/test_auto_pilot.py @@ -1,10 +1,9 @@ +from textual import events from textual.app import App from textual.pilot import Pilot -from textual import events def test_auto_pilot() -> None: - keys_pressed: list[str] = [] class TestApp(App): @@ -12,7 +11,6 @@ def test_auto_pilot() -> None: keys_pressed.append(event.key) async def auto_pilot(pilot: Pilot) -> None: - await pilot.press("tab", *"foo") await pilot.exit("bar") diff --git a/tests/test_binding.py b/tests/test_binding.py index 9a24b3e2d..65a8c0285 100644 --- a/tests/test_binding.py +++ b/tests/test_binding.py @@ -3,7 +3,7 @@ from string import ascii_lowercase import pytest from textual.app import App -from textual.binding import Bindings, Binding, BindingError, NoBinding, InvalidBinding +from textual.binding import Binding, BindingError, Bindings, InvalidBinding, NoBinding BINDING1 = Binding("a,b", action="action1", description="description1") BINDING2 = Binding("c", action="action2", description="description2") diff --git a/tests/test_border.py b/tests/test_border.py index 852e713ba..37133125b 100644 --- a/tests/test_border.py +++ b/tests/test_border.py @@ -5,7 +5,6 @@ from textual._border import render_row def test_border_render_row(): - style = Style.parse("red") row = (Segment("┏", style), Segment("━", style), Segment("┓", style)) diff --git a/tests/test_cache.py b/tests/test_cache.py index 1bf589755..384eccb23 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,5 +1,4 @@ -from __future__ import annotations -from __future__ import unicode_literals +from __future__ import annotations, unicode_literals import pytest diff --git a/tests/test_call_later.py b/tests/test_call_later.py index 025eb8c12..506db93a5 100644 --- a/tests/test_call_later.py +++ b/tests/test_call_later.py @@ -1,4 +1,5 @@ import asyncio + from textual.app import App diff --git a/tests/test_color.py b/tests/test_color.py index 10340d3dc..c4ef8311c 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -43,7 +43,6 @@ def test_rgb(): def test_hls(): - red = Color(200, 20, 32) print(red.hsl) assert red.hsl == pytest.approx( diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index c73418f2f..4dab38dc2 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -1,6 +1,7 @@ +from threading import Thread + import pytest -from threading import Thread from textual.app import App, ComposeResult from textual.widgets import TextLog diff --git a/tests/test_dom.py b/tests/test_dom.py index ded834300..f366d0706 100644 --- a/tests/test_dom.py +++ b/tests/test_dom.py @@ -1,7 +1,7 @@ import pytest from textual.css.errors import StyleValueError -from textual.dom import DOMNode, BadIdentifier +from textual.dom import BadIdentifier, DOMNode def test_display_default(): diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 6b9cd07b1..c8c663df5 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,6 +1,6 @@ import pytest -from textual.geometry import clamp, Offset, Size, Region, Spacing +from textual.geometry import Offset, Region, Size, Spacing, clamp def test_dimensions_region(): @@ -299,6 +299,7 @@ def test_size_line_range(): assert Size(20, 0).line_range == range(0) assert Size(0, 20).line_range == range(20) + def test_region_x_extents(): assert Region(5, 10, 20, 30).column_span == (5, 25) diff --git a/tests/test_immutable_sequence_view.py b/tests/test_immutable_sequence_view.py index fd691798c..5e3dffde8 100644 --- a/tests/test_immutable_sequence_view.py +++ b/tests/test_immutable_sequence_view.py @@ -1,6 +1,7 @@ +from typing import Sequence + import pytest -from typing import Sequence from textual._immutable_sequence_view import ImmutableSequenceView diff --git a/tests/test_loop.py b/tests/test_loop.py index c235f23a5..87ef97da7 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -1,4 +1,4 @@ -from textual._loop import loop_first, loop_last, loop_first_last +from textual._loop import loop_first, loop_first_last, loop_last def test_loop_first(): diff --git a/tests/test_node_list.py b/tests/test_node_list.py index 71001559f..37d6bfd28 100644 --- a/tests/test_node_list.py +++ b/tests/test_node_list.py @@ -1,7 +1,7 @@ import pytest -from textual.widget import Widget from textual._node_list import NodeList +from textual.widget import Widget def test_empty_list(): diff --git a/tests/test_overflow_change.py b/tests/test_overflow_change.py index 1ebd39765..236a15f8c 100644 --- a/tests/test_overflow_change.py +++ b/tests/test_overflow_change.py @@ -1,7 +1,6 @@ """Regression test for #1616 https://github.com/Textualize/textual/issues/1616""" import pytest - from textual.app import App from textual.containers import Vertical diff --git a/tests/test_paste.py b/tests/test_paste.py index 58c5da41d..e8d7be34e 100644 --- a/tests/test_paste.py +++ b/tests/test_paste.py @@ -1,5 +1,5 @@ -from textual.app import App from textual import events +from textual.app import App async def test_paste_app(): diff --git a/tests/test_path.py b/tests/test_path.py index 3b9b268ea..501ab0523 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,5 +1,7 @@ from __future__ import annotations + from pathlib import Path + import pytest from textual.app import App diff --git a/tests/test_query.py b/tests/test_query.py index c2368ba47..c58ffd575 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -2,8 +2,8 @@ import pytest from textual.app import App, ComposeResult from textual.containers import Container +from textual.css.query import InvalidQueryFormat, NoMatches, TooManyMatches, WrongType from textual.widget import Widget -from textual.css.query import InvalidQueryFormat, WrongType, NoMatches, TooManyMatches def test_query(): diff --git a/tests/test_reactive.py b/tests/test_reactive.py index 8a889c61a..da8be66ae 100644 --- a/tests/test_reactive.py +++ b/tests/test_reactive.py @@ -259,7 +259,6 @@ async def test_reactive_method_call_order(): async def test_premature_reactive_call(): - watcher_called = False class BrokenWidget(Widget): @@ -335,7 +334,6 @@ async def test_watch_compute(): watch_called: list[bool] = [] class Calculator(App): - numbers = var("0") show_ac = var(True) value = var("") diff --git a/tests/test_screens.py b/tests/test_screens.py index 707bad5df..e877c1ade 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -16,15 +16,17 @@ async def test_installed_screens(): SCREENS = { "home": Screen, # Screen type "one": Screen(), # Screen instance - "two": lambda: Screen() # Callable[[], Screen] + "two": lambda: Screen(), # Callable[[], Screen] } app = ScreensApp() async with app.run_test() as pilot: pilot.app.push_screen("home") # Instantiates and pushes the "home" screen - pilot.app.push_screen("one") # Pushes the pre-instantiated "one" screen + pilot.app.push_screen("one") # Pushes the pre-instantiated "one" screen pilot.app.push_screen("home") # Pushes the single instance of "home" screen - pilot.app.push_screen("two") # Calls the callable, pushes returned Screen instance + pilot.app.push_screen( + "two" + ) # Calls the callable, pushes returned Screen instance assert len(app.screen_stack) == 5 assert app.screen_stack[1] is app.screen_stack[3] @@ -40,10 +42,8 @@ async def test_installed_screens(): pilot.app.pop_screen() - @skip_py310 async def test_screens(): - app = App() app._set_active() diff --git a/tests/test_segment_tools.py b/tests/test_segment_tools.py index 630114ed9..befe4baae 100644 --- a/tests/test_segment_tools.py +++ b/tests/test_segment_tools.py @@ -1,7 +1,7 @@ from rich.segment import Segment from rich.style import Style -from textual._segment_tools import line_crop, line_trim, line_pad +from textual._segment_tools import line_crop, line_pad, line_trim def test_line_crop(): diff --git a/tests/test_strip.py b/tests/test_strip.py index 40f3975fe..82d7ec680 100644 --- a/tests/test_strip.py +++ b/tests/test_strip.py @@ -2,9 +2,9 @@ import pytest from rich.segment import Segment from rich.style import Style +from textual._filter import Monochrome from textual._segment_tools import NoCellPositionForIndex from textual.strip import Strip -from textual._filter import Monochrome def test_cell_length() -> None: diff --git a/tests/test_table.py b/tests/test_table.py index 827242e85..1290933d4 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -46,8 +46,8 @@ async def test_table_clear_with_columns() -> None: assert table.data == {} assert table.row_count == 0 -async def test_table_add_row() -> None: +async def test_table_add_row() -> None: app = TableApp() async with app.run_test(): table = app.query_one(DataTable) diff --git a/tests/test_test_runner.py b/tests/test_test_runner.py index 0435d31d9..b47bed98d 100644 --- a/tests/test_test_runner.py +++ b/tests/test_test_runner.py @@ -1,5 +1,5 @@ -from textual.app import App from textual import events +from textual.app import App async def test_run_test() -> None: diff --git a/tests/test_text_backend.py b/tests/test_text_backend.py index bf9296720..135931e3f 100644 --- a/tests/test_text_backend.py +++ b/tests/test_text_backend.py @@ -113,21 +113,25 @@ def test_query_cursor_right_cursor_at_end_returns_false(): editor = TextEditorBackend(CONTENT, len(CONTENT)) assert not editor.query_cursor_right() + def test_cursor_text_start_cursor_already_at_start(): editor = TextEditorBackend(CONTENT) assert not editor.cursor_text_start() assert editor.cursor_index == 0 + def test_cursor_text_start_cursor_in_middle(): editor = TextEditorBackend(CONTENT, 6) assert editor.cursor_text_start() assert editor.cursor_index == 0 + def test_cursor_text_end_cursor_already_at_end(): editor = TextEditorBackend(CONTENT, len(CONTENT)) assert not editor.cursor_text_end() assert editor.cursor_index == len(CONTENT) + def test_cursor_text_end_cursor_in_middle(): editor = TextEditorBackend(CONTENT, len(CONTENT)) assert not editor.cursor_text_end() @@ -140,6 +144,7 @@ def test_insert_at_cursor_cursor_at_start(): assert editor.content == "ABC" + CONTENT assert editor.cursor_index == len("ABC") + def test_insert_at_cursor_cursor_in_middle(): start_cursor_index = 6 editor = TextEditorBackend(CONTENT, start_cursor_index) @@ -154,6 +159,7 @@ def test_insert_at_cursor_cursor_at_end(): assert editor.content == CONTENT + "ABC" assert editor.cursor_index == len(editor.content) + def test_get_range(): editor = TextEditorBackend(CONTENT) assert editor.get_range(0, 5) == "Hello" diff --git a/tests/test_unmount.py b/tests/test_unmount.py index 13e2e79b3..6ef23f247 100644 --- a/tests/test_unmount.py +++ b/tests/test_unmount.py @@ -1,7 +1,7 @@ from __future__ import annotations -from textual.app import App, ComposeResult from textual import events +from textual.app import App, ComposeResult from textual.containers import Container from textual.screen import Screen diff --git a/tests/test_widget.py b/tests/test_widget.py index a06cf7857..79c4f466c 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -5,7 +5,7 @@ from textual.app import App, ComposeResult from textual.css.errors import StyleValueError from textual.css.query import NoMatches from textual.geometry import Size -from textual.widget import Widget, MountError +from textual.widget import MountError, Widget from textual.widgets import Label diff --git a/tests/test_widget_mount_point.py b/tests/test_widget_mount_point.py index 67a6269c3..eebfdec5a 100644 --- a/tests/test_widget_mount_point.py +++ b/tests/test_widget_mount_point.py @@ -1,6 +1,6 @@ import pytest -from textual.widget import Widget, MountError +from textual.widget import MountError, Widget class Content(Widget): @@ -12,7 +12,6 @@ class Body(Widget): def test_find_dom_spot(): - # Build up a "fake" DOM for an application. screen = Widget(name="Screen") header = Widget(name="Header", id="header") diff --git a/tests/test_widget_mounting.py b/tests/test_widget_mounting.py index 189c77c14..666dca537 100644 --- a/tests/test_widget_mounting.py +++ b/tests/test_widget_mounting.py @@ -1,9 +1,9 @@ import pytest from textual.app import App -from textual.widget import Widget, WidgetError, MountError -from textual.widgets import Static from textual.css.query import TooManyMatches +from textual.widget import MountError, Widget, WidgetError +from textual.widgets import Static class SelfOwn(Widget): diff --git a/tests/test_widget_removing.py b/tests/test_widget_removing.py index a33860c83..49b92e191 100644 --- a/tests/test_widget_removing.py +++ b/tests/test_widget_removing.py @@ -1,8 +1,9 @@ import asyncio + from textual.app import App -from textual.widget import Widget -from textual.widgets import Static, Button from textual.containers import Container +from textual.widget import Widget +from textual.widgets import Button, Static async def test_remove_single_widget(): diff --git a/tests/test_xterm_parser.py b/tests/test_xterm_parser.py index 1b426267b..92ff1f31c 100644 --- a/tests/test_xterm_parser.py +++ b/tests/test_xterm_parser.py @@ -5,13 +5,13 @@ import pytest from textual._xterm_parser import XTermParser from textual.events import ( - Paste, Key, MouseDown, - MouseUp, MouseMove, MouseScrollDown, MouseScrollUp, + MouseUp, + Paste, ) from textual.messages import TerminalSupportsSynchronizedOutput diff --git a/tests/tree/test_tree_get_node_by_id.py b/tests/tree/test_tree_get_node_by_id.py index ffee36996..62f481aa9 100644 --- a/tests/tree/test_tree_get_node_by_id.py +++ b/tests/tree/test_tree_get_node_by_id.py @@ -1,5 +1,7 @@ -import pytest from typing import cast + +import pytest + from textual.widgets import Tree from textual.widgets._tree import NodeID diff --git a/tests/tree/test_tree_messages.py b/tests/tree/test_tree_messages.py index df7442d31..6296126e8 100644 --- a/tests/tree/test_tree_messages.py +++ b/tests/tree/test_tree_messages.py @@ -1,9 +1,10 @@ from __future__ import annotations from typing import Any + from textual.app import App, ComposeResult -from textual.widgets import Tree from textual.message import Message +from textual.widgets import Tree class MyTree(Tree[None]): diff --git a/tests/tree/test_tree_node_children.py b/tests/tree/test_tree_node_children.py index d6c5c7e4e..e68656036 100644 --- a/tests/tree/test_tree_node_children.py +++ b/tests/tree/test_tree_node_children.py @@ -1,4 +1,5 @@ import pytest + from textual.widgets import Tree from textual.widgets.tree import TreeNode diff --git a/tests/tree/test_tree_node_label.py b/tests/tree/test_tree_node_label.py index 7d7a04329..8c67f316a 100644 --- a/tests/tree/test_tree_node_label.py +++ b/tests/tree/test_tree_node_label.py @@ -1,6 +1,7 @@ +from rich.text import Text + from textual.widgets import Tree from textual.widgets.tree import TreeNode -from rich.text import Text def test_tree_node_label() -> None: From f092e9f46c3c10efd8e97cc1c9cfc65047038837 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 9 Feb 2023 13:50:38 +0000 Subject: [PATCH 16/16] Reorder some imports --- src/textual/demo.py | 2 +- src/textual/widgets/__init__.py | 2 +- src/textual/widgets/__init__.pyi | 4 ++-- tests/test_focus.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/textual/demo.py b/src/textual/demo.py index 9f523ab4c..362937c02 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -18,12 +18,12 @@ from textual.containers import Container, Horizontal from textual.reactive import reactive, watch from textual.widgets import ( Button, - Switch, DataTable, Footer, Header, Input, Static, + Switch, TextLog, ) diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index e26881468..355641bee 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -9,7 +9,6 @@ from ..case import camel_to_snake # be able to "see" them. if typing.TYPE_CHECKING: from ._button import Button - from ._switch import Switch from ._data_table import DataTable from ._directory_tree import DirectoryTree from ._footer import Footer @@ -21,6 +20,7 @@ if typing.TYPE_CHECKING: from ._placeholder import Placeholder from ._pretty import Pretty from ._static import Static + from ._switch import Switch from ._text_log import TextLog from ._tree import Tree from ._welcome import Welcome diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index ad3b73fbb..00d64cf50 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -1,17 +1,17 @@ # This stub file must re-export every classes exposed in the __init__.py's `__all__` list: from ._button import Button as Button from ._data_table import DataTable as DataTable -from ._switch import Switch as Switch from ._directory_tree import DirectoryTree as DirectoryTree from ._footer import Footer as Footer from ._header import Header as Header +from ._input import Input as Input from ._label import Label as Label from ._list_view import ListView as ListView from ._list_item import ListItem as ListItem from ._placeholder import Placeholder as Placeholder from ._pretty import Pretty as Pretty from ._static import Static as Static -from ._input import Input as Input +from ._switch import Switch as Switch from ._text_log import TextLog as TextLog from ._tree import Tree as Tree from ._tree_node import TreeNode as TreeNode diff --git a/tests/test_focus.py b/tests/test_focus.py index cee50c6da..b4d9ce8c5 100644 --- a/tests/test_focus.py +++ b/tests/test_focus.py @@ -152,7 +152,7 @@ def test_focus_next_and_previous_with_type_selector_without_self(): screen = app.screen from textual.containers import Horizontal, Vertical - from textual.widgets import Button, Switch, Input + from textual.widgets import Button, Input, Switch screen._add_children( Vertical(