new logging

This commit is contained in:
Will McGugan
2022-09-01 11:11:03 +01:00
parent c76594409c
commit 433aaafdd2
22 changed files with 386 additions and 196 deletions

View File

@@ -35,4 +35,4 @@ class SmoothApp(App):
# self.set_timer(10, lambda: self.action("quit"))
SmoothApp.run(log_path="textual.log", log_verbosity=2)
SmoothApp.run(log_path="textual.log")

View File

@@ -23,7 +23,9 @@ class ButtonsApp(App[str]):
app = ButtonsApp(
log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=2
log_path="textual.log",
css_path="buttons.css",
watch_css=True,
)
if __name__ == "__main__":

View File

@@ -23,7 +23,9 @@ class ButtonsApp(App[str]):
app = ButtonsApp(
log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=2
log_path="textual.log",
css_path="buttons.css",
watch_css=True,
)
if __name__ == "__main__":

View File

@@ -62,9 +62,7 @@ class FileSearchApp(App):
self.file_table.filter = event.value
app = FileSearchApp(
log_path="textual.log", css_path="file_search.scss", watch_css=True, log_verbosity=2
)
app = FileSearchApp(log_path="textual.log", css_path="file_search.scss", watch_css=True)
if __name__ == "__main__":
result = app.run()

View File

@@ -61,9 +61,7 @@ class InputApp(App[str]):
self.celsius.value = f"{celsius:.1f}"
app = InputApp(
log_path="textual.log", css_path="input.scss", watch_css=True, log_verbosity=2
)
app = InputApp(log_path="textual.log", css_path="input.scss", watch_css=True)
if __name__ == "__main__":
result = app.run()

View File

@@ -9,8 +9,8 @@ Screen {
table-columns: 1fr;
table-rows: 2fr 1fr 1fr 1fr 1fr 1fr;
margin: 1 2;
min-height:26;
min-width: 50;
min-height:25;
min-width: 26;
}
Button {
@@ -18,18 +18,15 @@ Button {
height: 100%;
}
.display {
#numbers {
column-span: 4;
content-align: right middle;
padding: 0 1;
height: 100%;
background: $panel-darken-2;
background: $primary-lighten-2;
color: $text-primary-lighten-2;
}
.special {
tint: $text-panel 20%;
}
.zero {
#number-0 {
column-span: 2;
}

View File

@@ -1,35 +1,141 @@
from textual.app import App
from decimal import Decimal
from textual.app import App, ComposeResult
from textual import events
from textual.layout import Container
from textual.reactive import Reactive
from textual.widgets import Button, Static
class CalculatorApp(App):
def compose(self):
"""A working 'desktop' calculator."""
numbers = Reactive.var("0")
show_ac = Reactive.var(True)
left = Reactive.var(Decimal("0"))
right = Reactive.var(Decimal("0"))
value = Reactive.var("")
operator = Reactive.var("plus")
KEY_MAP = {
"+": "plus",
"-": "minus",
".": "point",
"*": "multiply",
"/": "divide",
"_": "plus-minus",
"%": "percent",
"=": "equals",
}
def watch_numbers(self, value: str) -> None:
"""Called when numbers is updated."""
# Update the Numbers widget
self.query_one("#numbers", Static).update(value)
def compute_show_ac(self) -> bool:
"""Compute switch to show AC or C button"""
return self.value in ("", "0") and self.numbers == "0"
def watch_show_ac(self, show_ac: bool) -> None:
"""Called when show_ac changes."""
self.query_one("#c").display = not show_ac
self.query_one("#ac").display = show_ac
def compose(self) -> ComposeResult:
"""Add our buttons."""
yield Container(
Static("0", classes="display"),
Button("AC", classes="special"),
Button("+/-", classes="special"),
Button("%", classes="special"),
Button("÷", variant="warning"),
Button("7"),
Button("8"),
Button("9"),
Button("×", variant="warning"),
Button("4"),
Button("5"),
Button("6"),
Button("-", variant="warning"),
Button("1"),
Button("2"),
Button("3"),
Button("+", variant="warning"),
Button("0", classes="operator zero"),
Button("."),
Button("=", variant="warning"),
Static(id="numbers"),
Button("AC", id="ac", variant="primary"),
Button("C", id="c", variant="primary"),
Button("+/-", id="plus-minus", variant="primary"),
Button("%", id="percent", variant="primary"),
Button("÷", id="divide", variant="warning"),
Button("7", id="number-7"),
Button("8", id="number-8"),
Button("9", id="number-9"),
Button("×", id="multiply", variant="warning"),
Button("4", id="number-4"),
Button("5", id="number-5"),
Button("6", id="number-6"),
Button("-", id="minus", variant="warning"),
Button("1", id="number-1"),
Button("2", id="number-2"),
Button("3", id="number-3"),
Button("+", id="plus", variant="warning"),
Button("0", id="number-0"),
Button(".", id="point"),
Button("=", id="equals", variant="warning"),
id="calculator",
)
def on_key(self, event: events.Key) -> None:
"""Called when the user presses a key."""
def press(button_id: str) -> None:
self.query_one(f"#{button_id}", Button).press()
self.set_focus(None)
key = event.key
if key.isdecimal():
press(f"number-{key}")
elif key == "c":
press("c")
press("ac")
elif key in self.KEY_MAP:
press(self.KEY_MAP[key])
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Called when a button is pressed."""
button_id = event.button.id
assert button_id is not None
self.bell() # Terminal bell
def do_math() -> None:
"""Does the math: LEFT OPERATOR RIGHT"""
try:
if self.operator == "plus":
self.left += self.right
elif self.operator == "minus":
self.left -= self.right
elif self.operator == "divide":
self.left /= self.right
elif self.operator == "multiply":
self.left *= self.right
self.numbers = str(self.left)
self.value = ""
except Exception:
self.numbers = "Error"
if button_id.startswith("number-"):
number = button_id.partition("-")[-1]
self.numbers = self.value = self.value.lstrip("0") + number
elif button_id == "plus-minus":
self.numbers = self.value = str(Decimal(self.value or "0") * -1)
elif button_id == "percent":
self.numbers = self.value = str(Decimal(self.value or "0") / Decimal(100))
elif button_id == "point":
if "." not in self.value:
self.numbers = self.value = (self.value or "0") + "."
elif button_id == "ac":
self.value = ""
self.left = self.right = Decimal(0)
self.operator = "plus"
self.numbers = "0"
elif button_id == "c":
self.value = ""
self.numbers = "0"
elif button_id in ("plus", "minus", "divide", "multiply"):
self.right = Decimal(self.value or "0")
do_math()
self.operator = button_id
elif button_id == "equals":
if self.value:
self.right = Decimal(self.value)
do_math()
app = CalculatorApp(css_path="calculator.css")
if __name__ == "__main__":

View File

@@ -52,4 +52,4 @@ class CenterApp(App):
)
app = CenterApp(log_verbosity=3)
app = CenterApp()

View File

@@ -37,9 +37,7 @@ class ButtonsApp(App[str]):
self.dark = not self.dark
app = ButtonsApp(
log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=3
)
app = ButtonsApp(log_path="textual.log", css_path="buttons.css", watch_css=True)
if __name__ == "__main__":
result = app.run()

View File

@@ -1,18 +1,108 @@
import inspect
from __future__ import annotations
import inspect
from typing import TYPE_CHECKING
import rich.repr
from rich.console import RenderableType
__all__ = ["log", "panic"]
def log(*args: object, verbosity: int = 0, **kwargs) -> None:
# TODO: There may be an early-out here for when there is no endpoint for logs
from ._context import active_app
if TYPE_CHECKING:
from .app import App
app = active_app.get()
from ._log import LogGroup, LogVerbosity, LogSeverity
caller = inspect.stack()[1]
app.log(*args, verbosity=verbosity, _textual_calling_frame=caller, **kwargs)
@rich.repr.auto
class Logger:
def __init__(
self,
app: App,
group: LogGroup = LogGroup.INFO,
verbosity: LogVerbosity = LogVerbosity.NORMAL,
severity: LogSeverity = LogSeverity.NORMAL,
) -> None:
self._app = app
self._group = group
self._verbosity = verbosity
self._severity = severity
def __rich_repr__(self) -> rich.repr.Result:
yield self._app
yield self._group, LogGroup.INFO
yield self._verbosity, LogVerbosity.NORMAL
yield self._severity, LogSeverity.NORMAL
def __call__(self, *args: object, **kwargs) -> None:
from ._context import active_app
app = active_app.get()
caller = inspect.stack()[1]
app._log(
self._group,
self._verbosity,
self._severity,
*args,
_textual_calling_frame=caller,
**kwargs,
)
def verbosity(self, verbose: bool) -> Logger:
"""Get a new logger with selective verbosity.
Args:
verbose (bool): True to use HIGH verbosity, otherwise NORMAL.
Returns:
Logger: New logger.
"""
verbosity = LogVerbosity.HIGH if verbose else LogVerbosity.NORMAL
return Logger(self._app, self._group, verbosity, LogSeverity.NORMAL)
@property
def verbose(self) -> Logger:
"""A verbose logger."""
return Logger(self._app, self._group, LogVerbosity.HIGH)
@property
def critical(self) -> Logger:
"""A critical logger."""
return Logger(self._app, self._group, self._verbosity, LogSeverity.CRITICAL)
@property
def event(self) -> Logger:
"""An event logger."""
return Logger(self._app, LogGroup.EVENT)
@property
def info(self) -> Logger:
"""An info logger."""
return Logger(self._app, LogGroup.INFO)
@property
def debug(self) -> Logger:
"""A debug logger."""
return Logger(self._app, LogGroup.DEBUG)
@property
def error(self) -> Logger:
"""An error logger."""
return Logger(self._app, LogGroup.ERROR)
log = Logger(LogGroup.INFO)
# def log(*args: object, verbosity: int = 0, **kwargs) -> None:
# # TODO: There may be an early-out here for when there is no endpoint for logs
# from ._context import active_app
# app = active_app.get()
# caller = inspect.stack()[1]
# app.log(*args, verbosity=verbosity, _textual_calling_frame=caller, **kwargs)
def panic(*args: RenderableType) -> None:

19
src/textual/_log.py Normal file
View File

@@ -0,0 +1,19 @@
from enum import Enum, auto
class LogGroup(Enum):
EVENT = auto()
DEBUG = auto()
INFO = auto()
WARNING = auto()
ERROR = auto()
class LogVerbosity(Enum):
NORMAL = 0
HIGH = 1
class LogSeverity(Enum):
NORMAL = 0
CRITICAL = 1

View File

@@ -7,7 +7,7 @@ import os
import platform
import sys
import warnings
from contextlib import redirect_stdout, redirect_stderr
from contextlib import redirect_stderr, redirect_stdout
from datetime import datetime
from pathlib import PurePath
from time import perf_counter
@@ -40,7 +40,16 @@ from rich.protocol import is_renderable
from rich.segment import Segments
from rich.traceback import Traceback
from . import actions, events, log, messages
from . import (
Logger,
LogSeverity,
LogGroup,
LogVerbosity,
actions,
events,
log,
messages,
)
from ._animator import Animator
from ._callback import invoke
from ._context import active_app
@@ -136,7 +145,6 @@ class App(Generic[ReturnType], DOMNode):
Args:
driver_class (Type[Driver] | None, optional): Driver class or ``None`` to auto-detect. Defaults to None.
log_path (str | PurePath, optional): Path to log file, or "" to disable. Defaults to "".
log_verbosity (int, optional): Log verbosity from 0-3. Defaults to 1.
title (str | None, optional): Title of the application. If ``None``, the title is set to the name of the ``App`` subclass. Defaults to ``None``.
css_path (str | PurePath | None, optional): Path to CSS or ``None`` for no CSS file. Defaults to None.
@@ -158,11 +166,6 @@ class App(Generic[ReturnType], DOMNode):
def __init__(
self,
driver_class: Type[Driver] | None = None,
log_path: str | PurePath = "",
log_verbosity: int = 0,
log_color_system: Literal[
"auto", "standard", "256", "truecolor", "windows"
] = "auto",
title: str | None = None,
css_path: str | PurePath | None = None,
watch_css: bool = False,
@@ -203,20 +206,7 @@ class App(Generic[ReturnType], DOMNode):
else:
self._title = title
self._log_console: Console | None = None
self._log_file: TextIO | None = None
if log_path:
self._log_file = open(log_path, "wt")
self._log_console = Console(
file=self._log_file,
color_system=log_color_system,
markup=False,
emoji=False,
highlight=False,
width=100,
)
self.log_verbosity = log_verbosity
self._logger = Logger(self)
self.bindings.bind("ctrl+c", "quit", show=False, allow_forward=False)
self._refresh_required = False
@@ -480,10 +470,16 @@ class App(Generic[ReturnType], DOMNode):
"""
return Size(*self.console.size)
def log(
@property
def log(self) -> Logger:
return self._logger
def _log(
self,
group: LogGroup,
verbosity: LogVerbosity,
severity: LogSeverity,
*objects: Any,
verbosity: int = 1,
_textual_calling_frame: inspect.FrameInfo | None = None,
**kwargs,
) -> None:
@@ -502,22 +498,24 @@ class App(Generic[ReturnType], DOMNode):
Args:
verbosity (int, optional): Verbosity level 0-3. Defaults to 1.
"""
if verbosity > self.log_verbosity:
return
if self._log_console is None and not self.devtools.is_connected:
if not self.devtools.is_connected:
return
if self.devtools.is_connected and not _textual_calling_frame:
if verbosity.value > LogVerbosity.NORMAL.value and not self.devtools.verbose:
return
if not _textual_calling_frame:
_textual_calling_frame = inspect.stack()[1]
try:
if len(objects) == 1 and not kwargs:
if self._log_console is not None:
self._log_console.print(objects[0])
if self.devtools.is_connected:
self.devtools.log(
DevtoolsLog(objects, caller=_textual_calling_frame)
)
self.devtools.log(
group,
verbosity,
severity,
DevtoolsLog(objects, caller=_textual_calling_frame),
)
else:
output = " ".join(str(arg) for arg in objects)
if kwargs:
@@ -525,12 +523,12 @@ 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
if self._log_console is not None:
self._log_console.print(output, soft_wrap=True)
if self.devtools.is_connected:
self.devtools.log(
DevtoolsLog(output, caller=_textual_calling_frame)
)
self.devtools.log(
group,
verbosity,
severity,
DevtoolsLog(output, caller=_textual_calling_frame),
)
except Exception as error:
self.on_exception(error)
@@ -687,8 +685,8 @@ class App(Generic[ReturnType], DOMNode):
self.log(f"<stylesheet> loaded {self.css_path!r} in {elapsed:.0f} ms")
except Exception as error:
# TODO: Catch specific exceptions
self.log.error(error)
self.bell()
self.log(error)
else:
self.stylesheet = stylesheet
self.reset_styles()
@@ -1043,7 +1041,6 @@ class App(Generic[ReturnType], DOMNode):
self.log("---")
self.log(driver=self.driver_class)
self.log(log_verbosity=self.log_verbosity)
self.log(loop=asyncio.get_running_loop())
self.log(features=self.features)
@@ -1060,7 +1057,7 @@ class App(Generic[ReturnType], DOMNode):
return
if self.css_monitor:
self.set_interval(0.5, self.css_monitor, name="css monitor")
self.set_interval(0.25, self.css_monitor, name="css monitor")
self.log("[b green]STARTED[/]", self.css_monitor)
process_messages = super()._process_messages
@@ -1097,7 +1094,7 @@ class App(Generic[ReturnType], DOMNode):
if self.is_headless:
await run_process_messages()
else:
redirector = StdoutRedirector(self.devtools, self._log_file)
redirector = StdoutRedirector(self.devtools)
with redirect_stderr(redirector):
with redirect_stdout(redirector): # type: ignore
await run_process_messages()
@@ -1110,13 +1107,6 @@ class App(Generic[ReturnType], DOMNode):
self._print_error_renderables()
if self.devtools.is_connected:
await self._disconnect_devtools()
if self._log_console is not None:
self._log_console.print(
f"Disconnected from devtools ({self.devtools.url})"
)
if self._log_file is not None:
self._log_file.close()
self._log_console = None
async def _ready(self) -> None:
"""Called immediately prior to processing messages.

View File

@@ -19,14 +19,15 @@ def run():
@run.command(help="Run the Textual Devtools console.")
def console():
@click.option("-v", "verbose", help="Enable verbose logs.", is_flag=True)
def console(verbose: bool = False):
from rich.console import Console
console = Console()
console.clear()
console.show_cursor(False)
try:
_run_devtools()
_run_devtools(verbose=verbose)
finally:
console.show_cursor(True)

View File

@@ -13,6 +13,8 @@ from typing import Type, Any, NamedTuple
from rich.console import Console
from rich.segment import Segment
from .._log import LogGroup, LogVerbosity, LogSeverity
class DevtoolsDependenciesMissingError(Exception):
"""Raise when the required devtools dependencies are not installed in the environment"""
@@ -115,6 +117,7 @@ class DevtoolsClient:
self.websocket: ClientWebSocketResponse | None = None
self.log_queue: Queue[str | bytes | Type[ClientShutdown]] | None = None
self.spillover: int = 0
self.verbose: bool = False
async def connect(self) -> None:
"""Connect to the devtools server.
@@ -150,6 +153,7 @@ class DevtoolsClient:
payload = message_json["payload"]
self.console.width = payload["width"]
self.console.height = payload["height"]
self.verbose = payload["verbose"]
async def send_queued_logs():
"""Coroutine function which is scheduled as a Task, which consumes
@@ -209,7 +213,13 @@ class DevtoolsClient:
return False
return not (self.session.closed or self.websocket.closed)
def log(self, log: DevtoolsLog) -> None:
def log(
self,
group: LogGroup,
verbosity: LogVerbosity,
severity: LogSeverity,
log: DevtoolsLog,
) -> None:
"""Queue a log to be sent to the devtools server for display.
Args:
@@ -227,6 +237,9 @@ class DevtoolsClient:
{
"type": "client_log",
"payload": {
"group": group.value,
"verbosity": verbosity.value,
"severity": severity.value,
"timestamp": int(time()),
"path": getattr(log.caller, "filename", ""),
"line_number": getattr(log.caller, "lineno", 0),

View File

@@ -17,16 +17,13 @@ class StdoutRedirector:
log file.
"""
def __init__(
self, devtools: DevtoolsClient, log_file: TextIOWrapper | None
) -> None:
def __init__(self, devtools: DevtoolsClient) -> None:
"""
Args:
devtools (DevtoolsClient): The running Textual app instance.
log_file (TextIOWrapper): The log file for the Textual App.
"""
self.devtools = devtools
self.log_file = log_file
self._buffer: list[DevtoolsLog] = []
def write(self, string: str) -> None:
@@ -38,7 +35,7 @@ class StdoutRedirector:
string (str): The string to write to the buffer.
"""
if not (self.devtools.is_connected or self.log_file is not None):
if not self.devtools.is_connected:
return
caller = inspect.stack()[1]
@@ -59,7 +56,6 @@ class StdoutRedirector:
the devtools server and the log file. In the case of the devtools,
where possible, log messages will be batched and sent as one.
"""
self._write_to_log_file()
self._write_to_devtools()
self._buffer.clear()
@@ -81,20 +77,6 @@ class StdoutRedirector:
if log_batch:
self._log_devtools_batched(log_batch)
def _write_to_log_file(self) -> None:
"""Write the contents of the buffer to the log file."""
if not self.log_file:
return
try:
log_text = "".join(str(log.objects_or_string) for log in self._buffer)
self.log_file.write(log_text)
self.log_file.flush()
except OSError:
# An error writing to the log file should not be
# considered fatal.
pass
def _log_devtools_batched(self, log_batch: list[DevtoolsLog]) -> None:
"""Write a single batch of logs to devtools. A batch means contiguous logs
which have been written from the same line number and file path.

View File

@@ -21,12 +21,15 @@ from rich.style import Style
from rich.table import Table
from rich.text import Text
from textual._border import Border
from textual._log import LogGroup
DevConsoleMessageLevel = Literal["info", "warning", "error"]
class DevConsoleHeader:
def __init__(self, verbose: bool = False) -> None:
self.verbose = verbose
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
@@ -35,6 +38,8 @@ class DevConsoleHeader:
"[magenta]Run a Textual app with [reverse]textual run --dev my_app.py[/] to connect.\n"
"[magenta]Press [reverse]Ctrl+C[/] to quit."
)
if self.verbose:
preamble.append(Text.from_markup("\n[cyan]Verbose logs enabled"))
render_options = options.update(width=options.max_width - 4)
lines = console.render_lines(preamble, render_options)
@@ -63,11 +68,17 @@ class DevConsoleLog:
path: str,
line_number: int,
unix_timestamp: int,
group: int,
verbosity: int,
severity: int,
) -> None:
self.segments = segments
self.path = path
self.line_number = line_number
self.unix_timestamp = unix_timestamp
self.group = group
self.verbosity = verbosity
self.severity = severity
def __rich_console__(
self, console: Console, options: ConsoleOptions
@@ -77,8 +88,14 @@ class DevConsoleLog:
file_link = escape(f"file://{Path(self.path).absolute()}")
file_and_line = escape(f"{Path(self.path).name}:{self.line_number}")
group = LogGroup(self.group).name
time = local_time.time()
table.add_row(
f"[dim]{local_time.time()}",
(
f":warning-emoji: [dim]{time} {group}"
if self.severity
else f"[dim][{time}] ({group.lower()})"
),
Align.right(
Text(f"{file_and_line}", style=Style(dim=True, link=file_link))
),

View File

@@ -36,8 +36,8 @@ async def _on_startup(app: Application) -> None:
await service.start()
def _run_devtools() -> None:
app = _make_devtools_aiohttp_app()
def _run_devtools(verbose: bool = False) -> None:
app = _make_devtools_aiohttp_app(verbose=verbose)
def noop_print(_: str):
return None
@@ -47,14 +47,16 @@ def _run_devtools() -> None:
def _make_devtools_aiohttp_app(
size_change_poll_delay_secs: float = DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS,
verbose: bool = False,
) -> Application:
app = Application()
app.on_shutdown.append(_on_shutdown)
app.on_startup.append(_on_startup)
app["verbose"] = verbose
app["service"] = DevtoolsService(
update_frequency=size_change_poll_delay_secs,
update_frequency=size_change_poll_delay_secs, verbose=verbose
)
app.add_routes(

View File

@@ -30,13 +30,15 @@ class DevtoolsService:
responsible for tracking connected client applications.
"""
def __init__(self, update_frequency: float) -> None:
def __init__(self, update_frequency: float, verbose: bool = False) -> None:
"""
Args:
update_frequency (float): The number of seconds to wait between
sending updates of the console size to connected clients.
verbose (bool): Enable verbose logging on client.
"""
self.update_frequency = update_frequency
self.verbose = verbose
self.console = Console()
self.shutdown_event = asyncio.Event()
self.clients: list[ClientHandler] = []
@@ -44,7 +46,7 @@ class DevtoolsService:
async def start(self):
"""Starts devtools tasks"""
self.size_poll_task = asyncio.create_task(self._console_size_poller())
self.console.print(DevConsoleHeader())
self.console.print(DevConsoleHeader(verbose=self.verbose))
@property
def clients_connected(self) -> bool:
@@ -58,6 +60,7 @@ class DevtoolsService:
"""
current_width = self.console.width
current_height = self.console.height
await self._send_server_info_to_all()
while not self.shutdown_event.is_set():
width = self.console.width
height = self.console.height
@@ -91,6 +94,7 @@ class DevtoolsService:
"payload": {
"width": self.console.width,
"height": self.console.height,
"verbose": self.verbose,
},
}
)
@@ -168,10 +172,8 @@ class ClientHandler:
type = message["type"]
if type == "client_log":
path = message["payload"]["path"]
line_number = message["payload"]["line_number"]
timestamp = message["payload"]["timestamp"]
encoded_segments = message["payload"]["segments"]
payload = message["payload"]
encoded_segments = payload["segments"]
segments = pickle.loads(encoded_segments)
message_time = time()
if (
@@ -183,9 +185,12 @@ class ClientHandler:
self.service.console.print(
DevConsoleLog(
segments=segments,
path=path,
line_number=line_number,
unix_timestamp=timestamp,
path=payload["path"],
line_number=payload["line_number"],
unix_timestamp=payload["timestamp"],
group=payload["group"],
verbosity=payload["verbosity"],
severity=payload["severity"],
)
)
last_message_time = message_time

View File

@@ -19,17 +19,17 @@ if TYPE_CHECKING:
@rich.repr.auto
class Event(Message, verbosity=2):
class Event(Message):
def __rich_repr__(self) -> rich.repr.Result:
return
yield
def __init_subclass__(cls, bubble: bool = True, verbosity: int = 1) -> None:
super().__init_subclass__(bubble=bubble, verbosity=verbosity)
def __init_subclass__(cls, bubble: bool = True, verbose: bool = False) -> None:
super().__init_subclass__(bubble=bubble, verbose=verbose)
@rich.repr.auto
class Callback(Event, bubble=False, verbosity=3):
class Callback(Event, bubble=False, verbose=True):
def __init__(
self, sender: MessageTarget, callback: Callable[[], Awaitable[None]]
) -> None:
@@ -40,7 +40,7 @@ class Callback(Event, bubble=False, verbosity=3):
yield "callback", self.callback
class InvokeCallbacks(Event, bubble=False):
class InvokeCallbacks(Event, bubble=False, verbose=True):
"""Sent after the Screen is updated"""
@@ -83,7 +83,7 @@ class Action(Event):
yield "action", self.action
class Resize(Event, verbosity=2, bubble=False):
class Resize(Event, bubble=False):
"""Sent when the app or widget has been resized.
Args:
sender (MessageTarget): The sender of the event (the Screen).
@@ -116,14 +116,10 @@ class Resize(Event, verbosity=2, bubble=False):
yield "container_size", self.container_size, self.size
class Mount(Event, bubble=False):
class Mount(Event, bubble=False, verbose=True):
"""Sent when a widget is *mounted* and may receive messages."""
class Unmount(Event, bubble=False):
"""Sent when a widget is unmounted, and may no longer receive messages."""
class Remove(Event, bubble=False):
"""Sent to a widget to ask it to remove itself from the DOM."""
@@ -217,7 +213,7 @@ class Key(InputEvent):
@rich.repr.auto
class MouseEvent(InputEvent, bubble=True, verbosity=2):
class MouseEvent(InputEvent, bubble=True):
"""Sent in response to a mouse event.
Args:
@@ -336,7 +332,7 @@ class MouseEvent(InputEvent, bubble=True, verbosity=2):
@rich.repr.auto
class MouseMove(MouseEvent, verbosity=3, bubble=True):
class MouseMove(MouseEvent, bubble=True, verbose=True):
"""Sent when the mouse cursor moves."""
@@ -350,7 +346,7 @@ class MouseUp(MouseEvent, bubble=True):
pass
class MouseScrollDown(InputEvent, verbosity=3, bubble=True):
class MouseScrollDown(InputEvent, bubble=True, verbose=True):
__slots__ = ["x", "y"]
def __init__(self, sender: MessageTarget, x: int, y: int) -> None:
@@ -359,7 +355,7 @@ class MouseScrollDown(InputEvent, verbosity=3, bubble=True):
self.y = y
class MouseScrollUp(InputEvent, verbosity=3, bubble=True):
class MouseScrollUp(InputEvent, bubble=True, verbose=True):
__slots__ = ["x", "y"]
def __init__(self, sender: MessageTarget, x: int, y: int) -> None:
@@ -373,7 +369,7 @@ class Click(MouseEvent, bubble=True):
@rich.repr.auto
class Timer(Event, verbosity=3, bubble=False):
class Timer(Event, bubble=False, verbose=True):
__slots__ = ["time", "count", "callback"]
def __init__(
@@ -395,11 +391,11 @@ class Timer(Event, verbosity=3, bubble=False):
yield "count", self.count
class Enter(Event, bubble=False):
class Enter(Event, bubble=False, verbose=True):
pass
class Leave(Event, bubble=False):
class Leave(Event, bubble=False, verbose=True):
pass
@@ -411,11 +407,11 @@ class Blur(Event, bubble=False):
pass
class DescendantFocus(Event, verbosity=2, bubble=True):
class DescendantFocus(Event, bubble=True):
pass
class DescendantBlur(Event, verbosity=2, bubble=True):
class DescendantBlur(Event, bubble=True):
pass

View File

@@ -30,7 +30,7 @@ class Message:
sender: MessageTarget
bubble: ClassVar[bool] = True # Message will bubble to parent
verbosity: ClassVar[int] = 1 # Verbosity (higher the more verbose)
verbose: ClassVar[bool] = False # Message is verbose
no_dispatch: ClassVar[bool] = False # Message may not be handled by client code
namespace: ClassVar[str] = "" # Namespace to disambiguate messages
@@ -52,15 +52,14 @@ class Message:
def __init_subclass__(
cls,
bubble: bool | None = True,
verbosity: int | None = 1,
verbose: bool = False,
no_dispatch: bool | None = False,
namespace: str | None = None,
) -> None:
super().__init_subclass__()
if bubble is not None:
cls.bubble = bubble
if verbosity is not None:
cls.verbosity = verbosity
cls.verbose = verbose
if no_dispatch is not None:
cls.no_dispatch = no_dispatch
if namespace is not None:

View File

@@ -15,7 +15,7 @@ from functools import partial
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable
from weakref import WeakSet
from . import events, log, messages
from . import events, log, messages, Logger
from ._callback import invoke
from ._context import NoActiveAppError, active_app
from .timer import Timer, TimerCallback
@@ -110,33 +110,9 @@ class MessagePump(metaclass=MessagePumpMeta):
def is_running(self) -> bool:
return self._running
def log(
self,
*args: Any,
verbosity: int = 1,
**kwargs,
) -> None:
"""Write to logs or devtools.
Positional args will logged. Keyword args will be prefixed with the key.
Example:
```python
data = [1,2,3]
self.log("Hello, World", state=data)
self.log(self.tree)
self.log(locals())
```
Args:
verbosity (int, optional): Verbosity level 0-3. Defaults to 1.
"""
return self.app.log(
*args,
**kwargs,
verbosity=verbosity,
_textual_calling_frame=inspect.stack()[1],
)
@property
def log(self) -> Logger:
return self.app._logger
def _attach(self, parent: MessagePump) -> None:
"""Set the parent, and therefore attach this node to the tree.
@@ -422,12 +398,11 @@ class MessagePump(metaclass=MessagePumpMeta):
# Look through the MRO to find a handler
for cls, method in self._get_dispatch_methods(handler_name, message):
log(
log.event.verbosity(message.verbose)(
message,
">>>",
self,
f"method=<{cls.__name__}.{handler_name}>",
verbosity=message.verbosity,
)
await invoke(method, message)

View File

@@ -13,7 +13,7 @@ if TYPE_CHECKING:
@rich.repr.auto
class Update(Message, verbosity=3):
class Update(Message, verbose=True):
def __init__(self, sender: MessagePump, widget: Widget):
super().__init__(sender)
self.widget = widget
@@ -33,13 +33,13 @@ class Update(Message, verbosity=3):
@rich.repr.auto
class Layout(Message, verbosity=3):
class Layout(Message, verbose=True):
def can_replace(self, message: Message) -> bool:
return isinstance(message, Layout)
@rich.repr.auto
class InvokeLater(Message, verbosity=3):
class InvokeLater(Message, verbose=True):
def __init__(self, sender: MessagePump, callback: CallbackType) -> None:
self.callback = callback
super().__init__(sender)