From 7e9b296c93e86c2c248129893d25ed7939f87e13 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 22 Oct 2022 17:04:48 +0100 Subject: [PATCH] handle missing dev --- docs/getting_started.md | 4 +-- pyproject.toml | 2 +- src/textual/__init__.py | 2 +- src/textual/app.py | 58 +++++++++++++++++++++++++--------- src/textual/cli/cli.py | 8 ++++- src/textual/demo.py | 2 +- src/textual/devtools/client.py | 23 ++++---------- src/textual/devtools/server.py | 16 +++++++--- 8 files changed, 73 insertions(+), 42 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 514d9628a..7c2817d56 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -25,13 +25,13 @@ You can install Textual via PyPI. If you plan on developing Textual apps, then you should install `textual[dev]`. The `[dev]` part installs a few extra dependencies for development. ``` -pip install "textual[dev]==0.2.0b9" +pip install "textual[dev]==0.2.0b11" ``` If you only plan on _running_ Textual apps, then you can drop the `[dev]` part: ``` -pip install textual==0.2.0b9 +pip install textual==0.2.0b11 ``` ## Demo diff --git a/pyproject.toml b/pyproject.toml index 4e09112a7..5a6fa824d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.2.0b9" +version = "0.2.0b11" homepage = "https://github.com/Textualize/textual" description = "Modern Text User Interface framework" authors = ["Will McGugan "] diff --git a/src/textual/__init__.py b/src/textual/__init__.py index b863208ee..19af96b0b 100644 --- a/src/textual/__init__.py +++ b/src/textual/__init__.py @@ -52,7 +52,7 @@ class Logger: app = active_app.get() except LookupError: raise LoggerError("Unable to log without an active app.") from None - if not app.devtools.is_connected: + if not app.devtools_enabled: return previous_frame = inspect.currentframe().f_back diff --git a/src/textual/app.py b/src/textual/app.py index 602e001f1..ef76c9fb0 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -12,7 +12,7 @@ from contextlib import redirect_stderr, redirect_stdout from datetime import datetime from pathlib import Path, PurePath from time import perf_counter -from typing import Any, Generic, Iterable, Type, TypeVar, cast, Union +from typing import Any, Generic, Iterable, Type, TYPE_CHECKING, TypeVar, cast, Union from weakref import WeakSet, WeakValueDictionary from ._ansi_sequences import SYNC_END, SYNC_START @@ -36,8 +36,6 @@ from .binding import Binding, Bindings from .css.query import NoMatches from .css.stylesheet import Stylesheet 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 @@ -51,6 +49,10 @@ from .renderables.blank import Blank from .screen import Screen from .widget import AwaitMount, Widget +if TYPE_CHECKING: + from .devtools.client import DevtoolsClient + + PLATFORM = platform.system() WINDOWS = PLATFORM == "Windows" @@ -221,7 +223,15 @@ class App(Generic[ReturnType], DOMNode): ] = WeakValueDictionary() self._installed_screens.update(**self.SCREENS) - self.devtools = DevtoolsClient() + self.devtools: DevtoolsClient | None = None + try: + from .devtools.client import DevtoolsClient + except ImportError: + # Dev dependencies not installed + self.devtools = None + else: + self.devtools = DevtoolsClient() + self._return_value: ReturnType | None = None self.css_monitor = ( @@ -275,7 +285,7 @@ class App(Generic[ReturnType], DOMNode): bool: True if devtools are enabled. """ - return "devtools" in self.features + return "devtools" in self.features and self.devtools is not None @property def debug(self) -> bool: @@ -448,15 +458,18 @@ class App(Generic[ReturnType], DOMNode): verbosity (int, optional): Verbosity level 0-3. Defaults to 1. """ - if not self.devtools.is_connected: + devtools = self.devtools + if devtools is None: return - if verbosity.value > LogVerbosity.NORMAL.value and not self.devtools.verbose: + if verbosity.value > LogVerbosity.NORMAL.value and not devtools.verbose: return try: + from .devtools.client import DevtoolsLog + if len(objects) == 1 and not kwargs: - self.devtools.log( + devtools.log( DevtoolsLog(objects, caller=_textual_calling_frame), group, verbosity, @@ -468,7 +481,7 @@ class App(Generic[ReturnType], DOMNode): f"{key}={value!r}" for key, value in kwargs.items() ) output = f"{output} {key_values}" if output else key_values - self.devtools.log( + devtools.log( DevtoolsLog(output, caller=_textual_calling_frame), group, verbosity, @@ -987,6 +1000,9 @@ class App(Generic[ReturnType], DOMNode): self._set_active() if self.devtools_enabled: + assert self.devtools is not None + from .devtools.client import DevtoolsConnectionError + try: await self.devtools.connect() self.log.system(f"Connected to devtools ( {self.devtools.url} )") @@ -1074,10 +1090,21 @@ class App(Generic[ReturnType], DOMNode): if self.is_headless: await run_process_messages() else: - redirector = StdoutRedirector(self.devtools) - with redirect_stderr(redirector): - with redirect_stdout(redirector): # type: ignore - await run_process_messages() + if self.devtools_enabled: + devtools = self.devtools + assert devtools is not None + from .devtools.redirect_output import StdoutRedirector + + redirector = StdoutRedirector(devtools) + with redirect_stderr(redirector): + with redirect_stdout(redirector): # type: ignore + await run_process_messages() + else: + null_file = _NullFile() + with redirect_stderr(null_file): + with redirect_stdout(null_file): + await run_process_messages() + finally: driver.stop_application_mode() except Exception as error: @@ -1085,7 +1112,7 @@ class App(Generic[ReturnType], DOMNode): finally: self._running = False self._print_error_renderables() - if self.devtools.is_connected: + if self.devtools is not None and self.devtools.is_connected: await self._disconnect_devtools() async def _pre_process(self) -> None: @@ -1185,7 +1212,8 @@ class App(Generic[ReturnType], DOMNode): self._registry.discard(widget) async def _disconnect_devtools(self): - await self.devtools.disconnect() + if self.devtools is not None: + await self.devtools.disconnect() def _start_widget(self, parent: Widget, widget: Widget) -> None: """Start a widget (run it's task) so that it can receive messages. diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index de15879a3..91ffa3270 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -1,8 +1,12 @@ from __future__ import annotations +import sys + import click from importlib_metadata import version -from textual.devtools.server import _run_devtools +from rich.console import Console + + from textual._import_app import import_app, AppFail @@ -16,7 +20,9 @@ def run(): @click.option("-v", "verbose", help="Enable verbose logs.", is_flag=True) @click.option("-x", "--exclude", "exclude", help="Exclude log group(s)", multiple=True) 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() console.clear() diff --git a/src/textual/demo.py b/src/textual/demo.py index 21bf4b8ba..adc24dcb3 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -383,8 +383,8 @@ class DemoApp(App): webbrowser.open(link) def action_toggle_sidebar(self) -> None: - sidebar = self.query_one(Sidebar) + self.set_focus(None) if sidebar.has_class("-hidden"): sidebar.remove_class("-hidden") else: diff --git a/src/textual/devtools/client.py b/src/textual/devtools/client.py index ca217e0df..6a3e77bc2 100644 --- a/src/textual/devtools/client.py +++ b/src/textual/devtools/client.py @@ -15,24 +15,15 @@ from rich.segment import Segment from .._log import LogGroup, LogVerbosity -class DevtoolsDependenciesMissingError(Exception): - """Raise when the required devtools dependencies are not installed in the environment""" +import aiohttp +import msgpack +from aiohttp import ( + ClientConnectorError, + ClientResponseError, + ClientWebSocketResponse, +) -try: - import aiohttp - import msgpack - from aiohttp import ( - ClientConnectorError, - ClientResponseError, - ClientWebSocketResponse, - ) -except ImportError: - # TODO: Add link to documentation on how to install devtools - raise DevtoolsDependenciesMissingError( - "Textual Devtools requires installation of the 'dev' extra dependencies. " - ) - DEVTOOLS_PORT = 8081 WEBSOCKET_CONNECT_TIMEOUT = 3 LOG_QUEUE_MAXSIZE = 512 diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py index f2e1ce32c..6ffe90445 100644 --- a/src/textual/devtools/server.py +++ b/src/textual/devtools/server.py @@ -1,11 +1,17 @@ from __future__ import annotations import asyncio -from aiohttp.web import run_app -from aiohttp.web_app import Application -from aiohttp.web_request import Request -from aiohttp.web_routedef import get -from aiohttp.web_ws import WebSocketResponse + +try: + from aiohttp.web import run_app + from aiohttp.web_app import Application + from aiohttp.web_request import Request + from aiohttp.web_routedef import get + from aiohttp.web_ws import WebSocketResponse +except ImportError: + raise ImportError( + "Textual Devtools requires installation of the 'dev' extra dependencies." + ) from textual.devtools.client import DEVTOOLS_PORT from textual.devtools.service import DevtoolsService