handle missing dev

This commit is contained in:
Will McGugan
2022-10-22 17:04:48 +01:00
parent c5a292aa9c
commit 7e9b296c93
8 changed files with 73 additions and 42 deletions

View File

@@ -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. 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: 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 ## Demo

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "textual" name = "textual"
version = "0.2.0b9" version = "0.2.0b11"
homepage = "https://github.com/Textualize/textual" homepage = "https://github.com/Textualize/textual"
description = "Modern Text User Interface framework" description = "Modern Text User Interface framework"
authors = ["Will McGugan <will@textualize.io>"] authors = ["Will McGugan <will@textualize.io>"]

View File

@@ -52,7 +52,7 @@ class Logger:
app = active_app.get() app = active_app.get()
except LookupError: except LookupError:
raise LoggerError("Unable to log without an active app.") from None raise LoggerError("Unable to log without an active app.") from None
if not app.devtools.is_connected: if not app.devtools_enabled:
return return
previous_frame = inspect.currentframe().f_back previous_frame = inspect.currentframe().f_back

View File

@@ -12,7 +12,7 @@ from contextlib import redirect_stderr, redirect_stdout
from datetime import datetime from datetime import datetime
from pathlib import Path, PurePath from pathlib import Path, PurePath
from time import perf_counter 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 weakref import WeakSet, WeakValueDictionary
from ._ansi_sequences import SYNC_END, SYNC_START from ._ansi_sequences import SYNC_END, SYNC_START
@@ -36,8 +36,6 @@ from .binding import Binding, Bindings
from .css.query import NoMatches from .css.query import NoMatches
from .css.stylesheet import Stylesheet from .css.stylesheet import Stylesheet
from .design import ColorSystem from .design import ColorSystem
from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog
from .devtools.redirect_output import StdoutRedirector
from .dom import DOMNode from .dom import DOMNode
from .driver import Driver from .driver import Driver
from .drivers.headless_driver import HeadlessDriver from .drivers.headless_driver import HeadlessDriver
@@ -51,6 +49,10 @@ from .renderables.blank import Blank
from .screen import Screen from .screen import Screen
from .widget import AwaitMount, Widget from .widget import AwaitMount, Widget
if TYPE_CHECKING:
from .devtools.client import DevtoolsClient
PLATFORM = platform.system() PLATFORM = platform.system()
WINDOWS = PLATFORM == "Windows" WINDOWS = PLATFORM == "Windows"
@@ -221,7 +223,15 @@ class App(Generic[ReturnType], DOMNode):
] = WeakValueDictionary() ] = WeakValueDictionary()
self._installed_screens.update(**self.SCREENS) 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._return_value: ReturnType | None = None
self.css_monitor = ( self.css_monitor = (
@@ -275,7 +285,7 @@ class App(Generic[ReturnType], DOMNode):
bool: True if devtools are enabled. bool: True if devtools are enabled.
""" """
return "devtools" in self.features return "devtools" in self.features and self.devtools is not None
@property @property
def debug(self) -> bool: def debug(self) -> bool:
@@ -448,15 +458,18 @@ class App(Generic[ReturnType], DOMNode):
verbosity (int, optional): Verbosity level 0-3. Defaults to 1. verbosity (int, optional): Verbosity level 0-3. Defaults to 1.
""" """
if not self.devtools.is_connected: devtools = self.devtools
if devtools is None:
return return
if verbosity.value > LogVerbosity.NORMAL.value and not self.devtools.verbose: if verbosity.value > LogVerbosity.NORMAL.value and not devtools.verbose:
return return
try: try:
from .devtools.client import DevtoolsLog
if len(objects) == 1 and not kwargs: if len(objects) == 1 and not kwargs:
self.devtools.log( devtools.log(
DevtoolsLog(objects, caller=_textual_calling_frame), DevtoolsLog(objects, caller=_textual_calling_frame),
group, group,
verbosity, verbosity,
@@ -468,7 +481,7 @@ class App(Generic[ReturnType], DOMNode):
f"{key}={value!r}" for key, value in kwargs.items() f"{key}={value!r}" for key, value in kwargs.items()
) )
output = f"{output} {key_values}" if output else key_values output = f"{output} {key_values}" if output else key_values
self.devtools.log( devtools.log(
DevtoolsLog(output, caller=_textual_calling_frame), DevtoolsLog(output, caller=_textual_calling_frame),
group, group,
verbosity, verbosity,
@@ -987,6 +1000,9 @@ class App(Generic[ReturnType], DOMNode):
self._set_active() self._set_active()
if self.devtools_enabled: if self.devtools_enabled:
assert self.devtools is not None
from .devtools.client import DevtoolsConnectionError
try: try:
await self.devtools.connect() await self.devtools.connect()
self.log.system(f"Connected to devtools ( {self.devtools.url} )") self.log.system(f"Connected to devtools ( {self.devtools.url} )")
@@ -1074,10 +1090,21 @@ class App(Generic[ReturnType], DOMNode):
if self.is_headless: if self.is_headless:
await run_process_messages() await run_process_messages()
else: else:
redirector = StdoutRedirector(self.devtools) if self.devtools_enabled:
with redirect_stderr(redirector): devtools = self.devtools
with redirect_stdout(redirector): # type: ignore assert devtools is not None
await run_process_messages() 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: finally:
driver.stop_application_mode() driver.stop_application_mode()
except Exception as error: except Exception as error:
@@ -1085,7 +1112,7 @@ class App(Generic[ReturnType], DOMNode):
finally: finally:
self._running = False self._running = False
self._print_error_renderables() self._print_error_renderables()
if self.devtools.is_connected: if self.devtools is not None and self.devtools.is_connected:
await self._disconnect_devtools() await self._disconnect_devtools()
async def _pre_process(self) -> None: async def _pre_process(self) -> None:
@@ -1185,7 +1212,8 @@ class App(Generic[ReturnType], DOMNode):
self._registry.discard(widget) self._registry.discard(widget)
async def _disconnect_devtools(self): 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: def _start_widget(self, parent: Widget, widget: Widget) -> None:
"""Start a widget (run it's task) so that it can receive messages. """Start a widget (run it's task) so that it can receive messages.

View File

@@ -1,8 +1,12 @@
from __future__ import annotations from __future__ import annotations
import sys
import click import click
from importlib_metadata import version 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 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("-v", "verbose", help="Enable verbose logs.", is_flag=True)
@click.option("-x", "--exclude", "exclude", help="Exclude log group(s)", multiple=True) @click.option("-x", "--exclude", "exclude", help="Exclude log group(s)", multiple=True)
def console(verbose: bool, exclude: list[str]) -> None: def console(verbose: bool, exclude: list[str]) -> None:
"""Launch the textual console."""
from rich.console import Console from rich.console import Console
from textual.devtools.server import _run_devtools
console = Console() console = Console()
console.clear() console.clear()

View File

@@ -383,8 +383,8 @@ class DemoApp(App):
webbrowser.open(link) webbrowser.open(link)
def action_toggle_sidebar(self) -> None: def action_toggle_sidebar(self) -> None:
sidebar = self.query_one(Sidebar) sidebar = self.query_one(Sidebar)
self.set_focus(None)
if sidebar.has_class("-hidden"): if sidebar.has_class("-hidden"):
sidebar.remove_class("-hidden") sidebar.remove_class("-hidden")
else: else:

View File

@@ -15,24 +15,15 @@ from rich.segment import Segment
from .._log import LogGroup, LogVerbosity from .._log import LogGroup, LogVerbosity
class DevtoolsDependenciesMissingError(Exception): import aiohttp
"""Raise when the required devtools dependencies are not installed in the environment""" 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 DEVTOOLS_PORT = 8081
WEBSOCKET_CONNECT_TIMEOUT = 3 WEBSOCKET_CONNECT_TIMEOUT = 3
LOG_QUEUE_MAXSIZE = 512 LOG_QUEUE_MAXSIZE = 512

View File

@@ -1,11 +1,17 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from aiohttp.web import run_app
from aiohttp.web_app import Application try:
from aiohttp.web_request import Request from aiohttp.web import run_app
from aiohttp.web_routedef import get from aiohttp.web_app import Application
from aiohttp.web_ws import WebSocketResponse 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.client import DEVTOOLS_PORT
from textual.devtools.service import DevtoolsService from textual.devtools.service import DevtoolsService