From c717dec982e05903d8e3a4decdd12b8b42a41eca Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 10:20:03 +0100 Subject: [PATCH 01/14] tests for auto refresh --- docs/examples/introduction/clock.py | 2 +- docs/examples/introduction/clock01.py | 8 ++-- src/textual/app.py | 54 ++++++++++++++++++--------- src/textual/dom.py | 23 ++++++++++++ src/textual/driver.py | 12 ------ src/textual/drivers/linux_driver.py | 10 +++++ src/textual/features.py | 15 +++----- src/textual/widget.py | 3 +- 8 files changed, 82 insertions(+), 45 deletions(-) diff --git a/docs/examples/introduction/clock.py b/docs/examples/introduction/clock.py index 8731d9d8c..3bfd17637 100644 --- a/docs/examples/introduction/clock.py +++ b/docs/examples/introduction/clock.py @@ -7,7 +7,7 @@ from textual.widget import Widget class Clock(Widget): def on_mount(self): self.styles.content_align = ("center", "middle") - self.set_interval(1, self.refresh) + self.auto_refresh = 1.0 def render(self): return datetime.now().strftime("%c") diff --git a/docs/examples/introduction/clock01.py b/docs/examples/introduction/clock01.py index 7a525c1ff..af5f33194 100644 --- a/docs/examples/introduction/clock01.py +++ b/docs/examples/introduction/clock01.py @@ -7,15 +7,15 @@ from textual.widget import Widget class Clock(Widget): def on_mount(self): self.styles.content_align = ("center", "middle") - self.set_interval(1, self.refresh) + self.auto_refresh = 1.0 def render(self): - return datetime.now().strftime("%c") + return datetime.now().strftime("%X") class ClockApp(App): - def on_mount(self): - self.mount(Clock()) + def compose(self): + yield Clock() app = ClockApp() diff --git a/src/textual/app.py b/src/textual/app.py index 9913e54d6..bca1aa682 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -47,6 +47,7 @@ from ._event_broker import NoHandler, extract_handler_actions from .binding import Bindings, NoBinding from .css.query import NoMatchingNodesError from .css.stylesheet import Stylesheet +from .drivers.headless_driver import HeadlessDriver from .design import ColorSystem from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog from .devtools.redirect_output import StdoutRedirector @@ -162,7 +163,7 @@ class App(Generic[ReturnType], DOMNode): _init_uvloop() super().__init__() - self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", "")) + self.features: set[FeatureFlag] = parse_features(os.getenv("TEXTUAL", "")) self.console = Console( file=(open(os.devnull, "wt") if self.is_headless else sys.__stdout__), @@ -539,17 +540,23 @@ class App(Generic[ReturnType], DOMNode): keys, action, description, show=show, key_display=key_display ) - def run(self, quit_after: float | None = None) -> ReturnType | None: + def run( + self, quit_after: float | None = None, headless: bool = False + ) -> ReturnType | None: """The main entry point for apps. Args: quit_after (float | None, optional): Quit after a given number of seconds, or None to run forever. Defaults to None. + headless (bool, optional): Run in "headless" mode (don't write to stdout). Returns: ReturnType | None: _description_ """ + if headless: + self.features.add("headless") + async def run_app() -> None: if quit_after is not None: self.set_timer(quit_after, self.shutdown) @@ -932,29 +939,40 @@ class App(Generic[ReturnType], DOMNode): self.set_interval(0.5, self.css_monitor, name="css monitor") self.log("[b green]STARTED[/]", self.css_monitor) + process_messages = super().process_messages + + async def run_process_messages(): + mount_event = events.Mount(sender=self) + await self.dispatch_message(mount_event) + + self.title = self._title + self.stylesheet.update(self) + self.refresh() + await self.animator.start() + await self._ready() + await process_messages() + await self.animator.stop() + await self.close_all() + self._running = True try: load_event = events.Load(sender=self) await self.dispatch_message(load_event) - driver = self._driver = self.driver_class(self.console, self) - driver.start_application_mode() - driver.enable_bracketed_paste() - try: - with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore - mount_event = events.Mount(sender=self) - await self.dispatch_message(mount_event) + driver: Driver + if self.is_headless: + driver = self._driver = HeadlessDriver(self.console, self) + else: + driver = self._driver = self.driver_class(self.console, self) - self.title = self._title - self.stylesheet.update(self) - self.refresh() - await self.animator.start() - await self._ready() - await super().process_messages() - await self.animator.stop() - await self.close_all() + driver.start_application_mode() + try: + if self.is_headless: + await run_process_messages() + else: + with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore + await run_process_messages() finally: - driver.disable_bracketed_paste() driver.stop_application_mode() except Exception as error: self.on_exception(error) diff --git a/src/textual/dom.py b/src/textual/dom.py index f1a0fc59f..68a14b28c 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -20,6 +20,7 @@ from .css.parse import parse_declarations from .css.styles import Styles, RenderStyles from .css.query import NoMatchingNodesError from .message_pump import MessagePump +from ._timer import Timer if TYPE_CHECKING: from .app import App @@ -71,8 +72,30 @@ class DOMNode(MessagePump): # A mapping of class names to Styles set in COMPONENT_CLASSES self._component_styles: dict[str, RenderStyles] = {} + self._auto_refresh: float | None = None + self._auto_refresh_timer: Timer | None = None + super().__init__() + @property + def auto_refresh(self) -> float | None: + return self._auto_refresh + + @auto_refresh.setter + def auto_refresh(self, interval: float | None) -> None: + if self._auto_refresh_timer is not None: + self._auto_refresh_timer.stop_no_wait() + self._auto_refresh_timer = None + if interval is not None: + self._auto_refresh_timer = self.set_interval( + interval, self._automatic_refresh + ) + self._auto_refresh = interval + + def _automatic_refresh(self) -> None: + """Perform an automatic refresh (set with auto_refresh property).""" + self.refresh() + def __init_subclass__(cls, inherit_css: bool = True) -> None: super().__init_subclass__() cls._inherit_css = inherit_css diff --git a/src/textual/driver.py b/src/textual/driver.py index 93e1fdd69..a349540f3 100644 --- a/src/textual/driver.py +++ b/src/textual/driver.py @@ -41,18 +41,6 @@ class Driver(ABC): click_event = events.Click.from_event(event) self.send_event(click_event) - def enable_bracketed_paste(self) -> None: - """Write the ANSI escape code `ESC[?2004h`, which - enables bracketed paste mode.""" - self.console.file.write("\x1b[?2004h") - self.console.file.flush() - - def disable_bracketed_paste(self) -> None: - """Write the ANSI escape code `ESC[?2004l`, which - disables bracketed paste mode.""" - self.console.file.write("\x1b[?2004l") - self.console.file.flush() - @abstractmethod def start_application_mode(self) -> None: ... diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 95a9dd1ed..b25c6e13d 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -70,6 +70,14 @@ class LinuxDriver(Driver): # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr # extensions. + def _enable_bracketed_paste(self) -> None: + """Enable bracketed paste mode.""" + self.console.file.write("\x1b[?2004h") + + def _disable_bracketed_paste(self) -> None: + """Disable bracketed pasgte mode.""" + self.console.file.write("\x1b[?2004l") + def _disable_mouse_support(self) -> None: write = self.console.file.write write("\x1b[?1000l") # @@ -130,6 +138,7 @@ class LinuxDriver(Driver): send_size_event() self._key_thread.start() self._request_terminal_sync_mode_support() + self._enable_bracketed_paste() def _request_terminal_sync_mode_support(self): self.console.file.write("\033[?2026$p") @@ -168,6 +177,7 @@ class LinuxDriver(Driver): pass def stop_application_mode(self) -> None: + self._disable_bracketed_paste() self.disable_input() if self.attrs_before is not None: diff --git a/src/textual/features.py b/src/textual/features.py index bc9b984db..e2c2506c9 100644 --- a/src/textual/features.py +++ b/src/textual/features.py @@ -14,7 +14,7 @@ FEATURES: Final = {"devtools", "debug", "headless"} FeatureFlag = Literal["devtools", "debug", "headless"] -def parse_features(features: str) -> frozenset[FeatureFlag]: +def parse_features(features: str) -> set[FeatureFlag]: """Parse features env var Args: @@ -24,11 +24,8 @@ def parse_features(features: str) -> frozenset[FeatureFlag]: frozenset[FeatureFlag]: A frozen set of known features. """ - features_set = frozenset( - set( - feature.strip().lower() - for feature in features.split(",") - if feature.strip() - ).intersection(FEATURES) - ) - return cast("frozenset[FeatureFlag]", features_set) + features_set = set( + feature.strip().lower() for feature in features.split(",") if feature.strip() + ).intersection(FEATURES) + + return cast("set[FeatureFlag]", features_set) diff --git a/src/textual/widget.py b/src/textual/widget.py index 1922d9c41..92ef7018b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -36,7 +36,8 @@ from .dom import DOMNode from .geometry import Offset, Region, Size, Spacing, clamp from .layouts.vertical import VerticalLayout from .message import Message -from .reactive import Reactive, watch +from .reactive import Reactive +from ._timer import Timer if TYPE_CHECKING: From 965f3447d58039a40613eecc6e297c5b6459bbfc Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 10:34:38 +0100 Subject: [PATCH 02/14] made features a frozenset --- src/textual/app.py | 7 +++++-- src/textual/drivers/windows_driver.py | 12 +++++++++++- src/textual/features.py | 6 +++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index bca1aa682..23b035cd9 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -14,6 +14,7 @@ from time import perf_counter from typing import ( TYPE_CHECKING, Any, + cast, Generic, Iterable, Iterator, @@ -163,7 +164,7 @@ class App(Generic[ReturnType], DOMNode): _init_uvloop() super().__init__() - self.features: set[FeatureFlag] = parse_features(os.getenv("TEXTUAL", "")) + self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", "")) self.console = Console( file=(open(os.devnull, "wt") if self.is_headless else sys.__stdout__), @@ -555,7 +556,9 @@ class App(Generic[ReturnType], DOMNode): """ if headless: - self.features.add("headless") + self.features = cast( + frozenset[FeatureFlag], self.features.union({"headless"}) + ) async def run_app() -> None: if quit_after is not None: diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py index e63aad542..54d250522 100644 --- a/src/textual/drivers/windows_driver.py +++ b/src/textual/drivers/windows_driver.py @@ -1,4 +1,4 @@ -from __future__ import annotations + from __future__ import annotations import asyncio import sys @@ -44,6 +44,14 @@ class WindowsDriver(Driver): write("\x1b[?1006l") self.console.file.flush() + def _enable_bracketed_paste(self) -> None: + """Enable bracketed paste mode.""" + self.console.file.write("\x1b[?2004h") + + def _disable_bracketed_paste(self) -> None: + """Disable bracketed pasgte mode.""" + self.console.file.write("\x1b[?2004l") + def start_application_mode(self) -> None: loop = asyncio.get_running_loop() @@ -54,6 +62,7 @@ class WindowsDriver(Driver): self._enable_mouse_support() self.console.show_cursor(False) self.console.file.write("\033[?1003h\n") + self._enable_bracketed_paste() app = active_app.get() @@ -75,6 +84,7 @@ class WindowsDriver(Driver): pass def stop_application_mode(self) -> None: + self._disable_bracketed_paste() self.disable_input() if self._restore_console: self._restore_console() diff --git a/src/textual/features.py b/src/textual/features.py index e2c2506c9..3d7aa6c0a 100644 --- a/src/textual/features.py +++ b/src/textual/features.py @@ -14,7 +14,7 @@ FEATURES: Final = {"devtools", "debug", "headless"} FeatureFlag = Literal["devtools", "debug", "headless"] -def parse_features(features: str) -> set[FeatureFlag]: +def parse_features(features: str) -> frozenset[FeatureFlag]: """Parse features env var Args: @@ -24,8 +24,8 @@ def parse_features(features: str) -> set[FeatureFlag]: frozenset[FeatureFlag]: A frozen set of known features. """ - features_set = set( + features_set = frozenset( feature.strip().lower() for feature in features.split(",") if feature.strip() ).intersection(FEATURES) - return cast("set[FeatureFlag]", features_set) + return cast("frozenset[FeatureFlag]", features_set) From de8569ecfbee93472c65f1d280d6d8ed4fe6b685 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 10:35:17 +0100 Subject: [PATCH 03/14] docstring --- src/textual/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index 23b035cd9..3739dbfd5 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -552,7 +552,7 @@ class App(Generic[ReturnType], DOMNode): headless (bool, optional): Run in "headless" mode (don't write to stdout). Returns: - ReturnType | None: _description_ + ReturnType | None: The return value specified in `App.exit` or None if exit wasn't called. """ if headless: From 5328d774733feabbe5fc8e04a2c3e17a15dc7a8c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 10:37:11 +0100 Subject: [PATCH 04/14] headless driver and tests --- src/textual/drivers/headless_driver.py | 17 +++++++++++++++ tests/test_auto_refresh.py | 30 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/textual/drivers/headless_driver.py create mode 100644 tests/test_auto_refresh.py diff --git a/src/textual/drivers/headless_driver.py b/src/textual/drivers/headless_driver.py new file mode 100644 index 000000000..ede6c4e8a --- /dev/null +++ b/src/textual/drivers/headless_driver.py @@ -0,0 +1,17 @@ +from __future__ import annotations + + +from ..driver import Driver + + +class HeadlessDriver(Driver): + """A do-nothing driver for testing.""" + + def start_application_mode(self) -> None: + pass + + def disable_input(self) -> None: + pass + + def stop_application_mode(self) -> None: + pass diff --git a/tests/test_auto_refresh.py b/tests/test_auto_refresh.py new file mode 100644 index 000000000..601c0aab8 --- /dev/null +++ b/tests/test_auto_refresh.py @@ -0,0 +1,30 @@ +from time import time + +from textual.app import App + + +class RefreshApp(App[float]): + def __init__(self): + self.count = 0 + super().__init__() + + def on_mount(self): + self.start = time() + self.auto_refresh = 0.1 + + def _automatic_refresh(self): + self.count += 1 + super()._automatic_refresh() + + def refresh(self, *, repaint: bool = True, layout: bool = False) -> None: + super().refresh(repaint=repaint, layout=layout) + if self.count == 3: + self.exit(time() - self.start) + + +def test_auto_refresh(): + app = RefreshApp() + + elapsed = app.run(quit_after=0.5, headless=True) + assert elapsed is not None + assert elapsed >= 0.3 and elapsed < 0.31 From bf38dc3e2eb826a3bfbcc45a48ed57f5037dff8b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 10:39:52 +0100 Subject: [PATCH 05/14] fix whitespace error --- src/textual/drivers/windows_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py index 54d250522..06ac22949 100644 --- a/src/textual/drivers/windows_driver.py +++ b/src/textual/drivers/windows_driver.py @@ -1,4 +1,4 @@ - from __future__ import annotations +from __future__ import annotations import asyncio import sys From 3631969f5de4aaa6aedcdda71008ab1ec9673ad7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 10:45:40 +0100 Subject: [PATCH 06/14] typing --- src/textual/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index 3739dbfd5..bbdfe503a 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -557,7 +557,7 @@ class App(Generic[ReturnType], DOMNode): if headless: self.features = cast( - frozenset[FeatureFlag], self.features.union({"headless"}) + "frozenset[FeatureFlag]", self.features.union({"headless"}) ) async def run_app() -> None: From 015c4e86c730f8ffab1aa3600b5b23638809624c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 10:48:53 +0100 Subject: [PATCH 07/14] relax constraint --- tests/test_auto_refresh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_auto_refresh.py b/tests/test_auto_refresh.py index 601c0aab8..09a8db22a 100644 --- a/tests/test_auto_refresh.py +++ b/tests/test_auto_refresh.py @@ -27,4 +27,4 @@ def test_auto_refresh(): elapsed = app.run(quit_after=0.5, headless=True) assert elapsed is not None - assert elapsed >= 0.3 and elapsed < 0.31 + assert elapsed >= 0.3 and elapsed < 0.35 From 48468e2576756459ab8cb1b0dc0b9d196990653c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 10:49:46 +0100 Subject: [PATCH 08/14] simplify test --- tests/test_auto_refresh.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_auto_refresh.py b/tests/test_auto_refresh.py index 09a8db22a..8358804a6 100644 --- a/tests/test_auto_refresh.py +++ b/tests/test_auto_refresh.py @@ -14,12 +14,9 @@ class RefreshApp(App[float]): def _automatic_refresh(self): self.count += 1 - super()._automatic_refresh() - - def refresh(self, *, repaint: bool = True, layout: bool = False) -> None: - super().refresh(repaint=repaint, layout=layout) if self.count == 3: self.exit(time() - self.start) + super()._automatic_refresh() def test_auto_refresh(): From 883e66aae5593c3bbbd7366545ee7eb1ab9b3dd4 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 10:53:57 +0100 Subject: [PATCH 09/14] relax constraint again --- tests/test_auto_refresh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_auto_refresh.py b/tests/test_auto_refresh.py index 8358804a6..b47daed40 100644 --- a/tests/test_auto_refresh.py +++ b/tests/test_auto_refresh.py @@ -24,4 +24,5 @@ def test_auto_refresh(): elapsed = app.run(quit_after=0.5, headless=True) assert elapsed is not None - assert elapsed >= 0.3 and elapsed < 0.35 + # CI can run slower, so we need to give this a bit of margin + assert elapsed >= 0.3 and elapsed < 0.5 From 709c2ae961f569b208290861c9409b4ce67df399 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 11:00:32 +0100 Subject: [PATCH 10/14] cleaner logic re driver --- src/textual/app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index bbdfe503a..6cc893fe7 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -963,10 +963,11 @@ class App(Generic[ReturnType], DOMNode): await self.dispatch_message(load_event) driver: Driver - if self.is_headless: - driver = self._driver = HeadlessDriver(self.console, self) - else: - driver = self._driver = self.driver_class(self.console, self) + driver_class = cast( + "type[Driver]", + HeadlessDriver if self.is_headless else self.driver_class, + ) + driver = self._driver = driver_class(self.console, self) driver.start_application_mode() try: From ea5af6b668ad24b2eea32395939bc5d86a4c1aff Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 11:04:15 +0100 Subject: [PATCH 11/14] raise quit after --- tests/test_auto_refresh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_auto_refresh.py b/tests/test_auto_refresh.py index b47daed40..5bde49e2a 100644 --- a/tests/test_auto_refresh.py +++ b/tests/test_auto_refresh.py @@ -22,7 +22,7 @@ class RefreshApp(App[float]): def test_auto_refresh(): app = RefreshApp() - elapsed = app.run(quit_after=0.5, headless=True) + elapsed = app.run(quit_after=1, headless=True) assert elapsed is not None # CI can run slower, so we need to give this a bit of margin assert elapsed >= 0.3 and elapsed < 0.5 From e2c2f1abab2238b08a8c9b2245455c3b207bc840 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 11:10:43 +0100 Subject: [PATCH 12/14] relax constraint again --- tests/test_auto_refresh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_auto_refresh.py b/tests/test_auto_refresh.py index 5bde49e2a..5d211224c 100644 --- a/tests/test_auto_refresh.py +++ b/tests/test_auto_refresh.py @@ -25,4 +25,4 @@ def test_auto_refresh(): elapsed = app.run(quit_after=1, headless=True) assert elapsed is not None # CI can run slower, so we need to give this a bit of margin - assert elapsed >= 0.3 and elapsed < 0.5 + assert elapsed >= 0.3 and elapsed < 0.6 From 45cd6834347e4b665e8b8a1df616cf8523c37389 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 11:14:33 +0100 Subject: [PATCH 13/14] typo --- src/textual/drivers/linux_driver.py | 2 +- src/textual/drivers/windows_driver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index b25c6e13d..11d6067de 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -75,7 +75,7 @@ class LinuxDriver(Driver): self.console.file.write("\x1b[?2004h") def _disable_bracketed_paste(self) -> None: - """Disable bracketed pasgte mode.""" + """Disable bracketed paste mode.""" self.console.file.write("\x1b[?2004l") def _disable_mouse_support(self) -> None: diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py index 06ac22949..fb51973ea 100644 --- a/src/textual/drivers/windows_driver.py +++ b/src/textual/drivers/windows_driver.py @@ -49,7 +49,7 @@ class WindowsDriver(Driver): self.console.file.write("\x1b[?2004h") def _disable_bracketed_paste(self) -> None: - """Disable bracketed pasgte mode.""" + """Disable bracketed paste mode.""" self.console.file.write("\x1b[?2004l") def start_application_mode(self) -> None: From 9c6aa6b52d58930badb435d357cd86b24b0dec3a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 11:15:30 +0100 Subject: [PATCH 14/14] cosmetic --- src/textual/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 6cc893fe7..5e5decfb9 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -14,13 +14,13 @@ from time import perf_counter from typing import ( TYPE_CHECKING, Any, - cast, Generic, Iterable, Iterator, TextIO, Type, TypeVar, + cast, ) from weakref import WeakSet, WeakValueDictionary @@ -48,12 +48,12 @@ from ._event_broker import NoHandler, extract_handler_actions from .binding import Bindings, NoBinding from .css.query import NoMatchingNodesError from .css.stylesheet import Stylesheet -from .drivers.headless_driver import HeadlessDriver from .design import ColorSystem from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog from .devtools.redirect_output import StdoutRedirector from .dom import DOMNode from .driver import Driver +from .drivers.headless_driver import HeadlessDriver from .features import FeatureFlag, parse_features from .file_monitor import FileMonitor from .geometry import Offset, Region, Size