Textual console CLI command

This commit is contained in:
Darren Burns
2022-04-19 11:55:50 +01:00
parent 1d01029cd7
commit 2ff1c9d64a
7 changed files with 66 additions and 22 deletions

View File

@@ -17,6 +17,9 @@ classifiers = [
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
] ]
[tool.poetry.scripts]
textual = "textual.cli.cli:run"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = "^3.7"
rich = "^12.0.0" rich = "^12.0.0"

View File

@@ -3,6 +3,7 @@ import inspect
from rich.console import RenderableType from rich.console import RenderableType
__all__ = ["log", "panic"] __all__ = ["log", "panic"]
__version__ = "0.1.15"
def log(*args: object, verbosity: int = 0, **kwargs) -> None: def log(*args: object, verbosity: int = 0, **kwargs) -> None:

13
src/textual/cli/cli.py Normal file
View File

@@ -0,0 +1,13 @@
import click
from textual.devtools.server import _run_devtools
@click.group()
def run():
pass
@run.command(help="Run the Textual Devtools console")
def console():
_run_devtools()

View File

@@ -5,9 +5,12 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Iterable from typing import Iterable
from rich.containers import Renderables
from rich.style import Style from rich.style import Style
from rich.text import Text from rich.text import Text
import textual
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
from typing import Literal from typing import Literal
else: else:
@@ -21,10 +24,31 @@ from rich.rule import Rule
from rich.segment import Segment, Segments from rich.segment import Segment, Segments
from rich.table import Table from rich.table import Table
DevtoolsMessageLevel = Literal["info", "warning", "error"] DevConsoleMessageLevel = Literal["info", "warning", "error"]
class DevtoolsLogMessage: class DevConsoleHeader:
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
lines = Renderables(
[
f"[bold]Textual Development Console [#b169dd]v{textual.__version__}",
"[#967fa3]Run a Textual app with the environment variable [b]TEXTUAL_DEVTOOLS=1[/] to connect.",
"[#967fa3]Press [b]Ctrl+C[/] to quit.",
]
)
render_options = options.update(width=options.max_width - 4)
lines = console.render_lines(lines, render_options)
new_line = Segment("\n")
padding = Segment("", Style.parse("#b169dd"))
for line in lines:
yield padding
yield from line
yield new_line
class DevConsoleLog:
"""Renderable representing a single log message """Renderable representing a single log message
Args: Args:
@@ -71,8 +95,8 @@ class DevtoolsLogMessage:
yield Segments(self.segments) yield Segments(self.segments)
class DevtoolsInternalMessage: class DevConsoleNotice:
"""Renderable for messages written by the devtools server itself """Renderable for messages written by the devtools console itself
Args: Args:
message (str): The message to display message (str): The message to display
@@ -80,7 +104,7 @@ class DevtoolsInternalMessage:
Determines colors used to render the message and the perceived importance. Determines colors used to render the message and the perceived importance.
""" """
def __init__(self, message: str, *, level: DevtoolsMessageLevel = "info") -> None: def __init__(self, message: str, *, level: DevConsoleMessageLevel = "info") -> None:
self.message = message self.message = message
self.level = level self.level = level

View File

@@ -38,7 +38,11 @@ async def _on_startup(app: Application) -> None:
def _run_devtools() -> None: def _run_devtools() -> None:
app = _make_devtools_aiohttp_app() app = _make_devtools_aiohttp_app()
run_app(app, port=DEVTOOLS_PORT)
def noop_print(_: str):
return None
run_app(app, port=DEVTOOLS_PORT, print=noop_print)
def _make_devtools_aiohttp_app( def _make_devtools_aiohttp_app(

View File

@@ -14,7 +14,11 @@ from aiohttp.web_ws import WebSocketResponse
from rich.console import Console from rich.console import Console
from rich.markup import escape from rich.markup import escape
from textual.devtools.renderables import DevtoolsLogMessage, DevtoolsInternalMessage from textual.devtools.renderables import (
DevConsoleLog,
DevConsoleNotice,
DevConsoleHeader,
)
QUEUEABLE_TYPES = {"client_log", "client_spillover"} QUEUEABLE_TYPES = {"client_log", "client_spillover"}
@@ -38,6 +42,7 @@ class DevtoolsService:
async def start(self): async def start(self):
"""Starts devtools tasks""" """Starts devtools tasks"""
self.size_poll_task = asyncio.create_task(self._console_size_poller()) self.size_poll_task = asyncio.create_task(self._console_size_poller())
self.console.print(DevConsoleHeader())
@property @property
def clients_connected(self) -> bool: def clients_connected(self) -> bool:
@@ -167,7 +172,7 @@ class ClientHandler:
decoded_segments = base64.b64decode(encoded_segments) decoded_segments = base64.b64decode(encoded_segments)
segments = pickle.loads(decoded_segments) segments = pickle.loads(decoded_segments)
self.service.console.print( self.service.console.print(
DevtoolsLogMessage( DevConsoleLog(
segments=segments, segments=segments,
path=path, path=path,
line_number=line_number, line_number=line_number,
@@ -176,7 +181,7 @@ class ClientHandler:
) )
elif type == "client_spillover": elif type == "client_spillover":
spillover = int(message_json["payload"]["spillover"]) spillover = int(message_json["payload"]["spillover"])
info_renderable = DevtoolsInternalMessage( info_renderable = DevConsoleNotice(
f"Discarded {spillover} messages", level="warning" f"Discarded {spillover} messages", level="warning"
) )
self.service.console.print(info_renderable) self.service.console.print(info_renderable)
@@ -198,9 +203,7 @@ class ClientHandler:
if self.request.remote: if self.request.remote:
self.service.console.print( self.service.console.print(
DevtoolsInternalMessage( DevConsoleNotice(f"Client '{escape(self.request.remote)}' connected")
f"Client '{escape(self.request.remote)}' connected"
)
) )
try: try:
await self.service.send_server_info(client_handler=self) await self.service.send_server_info(client_handler=self)
@@ -223,20 +226,16 @@ class ClientHandler:
await self.incoming_queue.put(message_json) await self.incoming_queue.put(message_json)
elif message.type == WSMsgType.ERROR: elif message.type == WSMsgType.ERROR:
self.service.console.print( self.service.console.print(
DevtoolsInternalMessage( DevConsoleNotice("Websocket error occurred", level="error")
"Websocket error occurred", level="error"
)
) )
break break
except Exception as error: except Exception as error:
self.service.console.print( self.service.console.print(DevConsoleNotice(str(error), level="error"))
DevtoolsInternalMessage(str(error), level="error")
)
finally: finally:
if self.request.remote: if self.request.remote:
self.service.console.print( self.service.console.print(
"\n", "\n",
DevtoolsInternalMessage( DevConsoleNotice(
f"Client '{escape(self.request.remote)}' disconnected" f"Client '{escape(self.request.remote)}' disconnected"
), ),
) )

View File

@@ -7,7 +7,7 @@ from rich.console import Console
from rich.segment import Segment from rich.segment import Segment
from tests.utilities.render import wait_for_predicate from tests.utilities.render import wait_for_predicate
from textual.devtools.renderables import DevtoolsLogMessage, DevtoolsInternalMessage from textual.devtools.renderables import DevConsoleLog, DevConsoleNotice
TIMESTAMP = 1649166819 TIMESTAMP = 1649166819
WIDTH = 40 WIDTH = 40
@@ -31,7 +31,7 @@ def console():
@time_machine.travel(TIMESTAMP) @time_machine.travel(TIMESTAMP)
def test_log_message_render(console): def test_log_message_render(console):
message = DevtoolsLogMessage( message = DevConsoleLog(
[Segment("content")], [Segment("content")],
path="abc/hello.py", path="abc/hello.py",
line_number=123, line_number=123,
@@ -62,7 +62,7 @@ def test_log_message_render(console):
def test_internal_message_render(console): def test_internal_message_render(console):
message = DevtoolsInternalMessage("hello") message = DevConsoleNotice("hello")
rule = next(iter(message.__rich_console__(console, console.options))) rule = next(iter(message.__rich_console__(console, console.options)))
assert rule.title == "hello" assert rule.title == "hello"
assert rule.characters == "" assert rule.characters == ""