diff --git a/docs/examples/introduction/timers.css b/docs/examples/introduction/timers.css index 5d6272b7e..ff92cc771 100644 --- a/docs/examples/introduction/timers.css +++ b/docs/examples/introduction/timers.css @@ -5,7 +5,7 @@ TimerWidget { border: tall $panel-darken-2; margin: 1; padding: 0 1; - transition: background 200ms linear; + transition: background 300ms linear; } TimerWidget.started { diff --git a/docs/examples/introduction/timers.py b/docs/examples/introduction/timers.py index fc1abe5a0..d990631ef 100644 --- a/docs/examples/introduction/timers.py +++ b/docs/examples/introduction/timers.py @@ -12,6 +12,7 @@ class TimeDisplay(Static): time_delta = Reactive(0.0) def watch_time_delta(self, time_delta: float) -> None: + """Called when time_delta changes.""" minutes, seconds = divmod(time_delta, 60) hours, minutes = divmod(minutes, 60) self.update(f"{hours:02.0f}:{minutes:02.0f}:{seconds:05.2f}") @@ -25,20 +26,24 @@ class TimerWidget(Static): started = Reactive(False) def on_mount(self) -> None: + """Called when widget is first added.""" self.update_timer = self.set_interval(1 / 30, self.update_elapsed, pause=True) def update_elapsed(self) -> None: + """Updates elapsed time.""" self.query_one(TimeDisplay).time_delta = ( self.total + monotonic() - self.start_time if self.started else self.total ) def compose(self) -> ComposeResult: + """Composes the timer widget.""" yield Button("Start", id="start", variant="success") yield Button("Stop", id="stop", variant="error") yield TimeDisplay() yield Button("Reset", id="reset") def watch_started(self, started: bool) -> None: + """Called when the 'started' attribute changes.""" if started: self.start_time = monotonic() self.update_timer.resume() @@ -51,6 +56,7 @@ class TimerWidget(Static): self.query_one("#start").focus() def on_button_pressed(self, event: Button.Pressed) -> None: + """Called when a button is pressed.""" button_id = event.button.id self.started = button_id == "start" if button_id == "reset": @@ -62,20 +68,24 @@ class TimerApp(App): """Manage the timers.""" def on_load(self) -> None: + """Called when the app first loads.""" self.bind("a", "add_timer", description="Add") self.bind("r", "remove_timer", description="Remove") def compose(self) -> ComposeResult: + """Called to ad widgets to the app.""" yield Header() yield Footer() yield Container(TimerWidget(), TimerWidget(), TimerWidget(), id="timers") def action_add_timer(self) -> None: + """An action to add a timer.""" new_timer = TimerWidget() self.query_one("#timers").mount(new_timer) new_timer.scroll_visible() def action_remove_timer(self) -> None: + """Called to remove a timer.""" timers = self.query("#timers TimerWidget") if timers: timers.last().remove() diff --git a/src/textual/dom.py b/src/textual/dom.py index 19e9d00f2..630c50aee 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1,7 +1,16 @@ from __future__ import annotations from inspect import getfile -from typing import ClassVar, Iterable, Iterator, Type, overload, TypeVar, TYPE_CHECKING +from typing import ( + cast, + ClassVar, + Iterable, + Iterator, + Type, + overload, + TypeVar, + TYPE_CHECKING, +) import rich.repr from rich.highlighter import ReprHighlighter @@ -29,6 +38,14 @@ if TYPE_CHECKING: from .widget import Widget +class DOMError(Exception): + pass + + +class NoScreen(DOMError): + pass + + class NoParent(Exception): pass @@ -197,7 +214,8 @@ class DOMNode(MessagePump): Returns: DOMNode | None: The node which is the direct parent of this node. """ - return self._parent + + return cast("DOMNode | None", self._parent) @property def screen(self) -> "Screen": @@ -209,7 +227,8 @@ class DOMNode(MessagePump): node = self while node and not isinstance(node, Screen): node = node._parent - assert isinstance(node, Screen) + if not isinstance(node, Screen): + raise NoScreen("{self} has no screen") return node @property diff --git a/src/textual/reactive.py b/src/textual/reactive.py index a5d52e70a..f16dc78c3 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -73,7 +73,7 @@ class Reactive(Generic[ReactiveType]): if callable(validate_function): value = validate_function(value) if current_value != value or first_set: - setattr(obj, f"{self.internal_name}__first_set", True) + setattr(obj, f"{self.internal_name}__first_set", False) setattr(obj, self.internal_name, value) self.check_watchers(obj, name, current_value) if self.layout or self.repaint: diff --git a/src/textual/widget.py b/src/textual/widget.py index 9d4a2938a..73cc137c6 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -37,7 +37,7 @@ from .geometry import Offset, Region, Size, Spacing, clamp from .layouts.vertical import VerticalLayout from .message import Message from .reactive import Reactive -from ._timer import Timer +from .dom import NoScreen if TYPE_CHECKING: @@ -516,6 +516,8 @@ class Widget(DOMNode): """The region occupied by this widget, relative to the Screen.""" try: return self.screen.find_widget(self).region + except NoScreen: + return Region() except errors.NoWidget: return Region() @@ -526,6 +528,8 @@ class Widget(DOMNode): """ try: return self.screen.find_widget(self).virtual_region + except NoScreen: + return Region() except errors.NoWidget: return Region()