Skip to content

Commit

Permalink
mount order (#2702)
Browse files Browse the repository at this point in the history
* mount order

* fix test

* simplify hooks

* changelog

* docstring
  • Loading branch information
willmcgugan authored May 31, 2023
1 parent 4ff1d18 commit 0849e6f
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 10 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fix crash when `Select` widget value attribute was set in `compose` https://github.com/Textualize/textual/pull/2690
- Issue with computing progress in workers https://github.com/Textualize/textual/pull/2686
- Issues with `switch_screen` not updating the results callback appropriately https://github.com/Textualize/textual/issues/2650

- Fixed incorrect mount order https://github.com/Textualize/textual/pull/2702

### Added

Expand All @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- `Input` has a new component class `input--suggestion` https://github.com/Textualize/textual/pull/2604
- Added `Widget.remove_children` https://github.com/Textualize/textual/pull/2657
- Added `Validator` framework and validation for `Input` https://github.com/Textualize/textual/pull/2600
- Added `message_hook` to App.run_test https://github.com/Textualize/textual/pull/2702

### Changed

Expand Down
4 changes: 3 additions & 1 deletion src/textual/_context.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from contextvars import ContextVar
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Callable

if TYPE_CHECKING:
from .app import App
Expand All @@ -21,3 +21,5 @@ class NoActiveAppError(RuntimeError):
)
visible_screen_stack: ContextVar[list[Screen]] = ContextVar("visible_screen_stack")
"""A stack of visible screens (with background alpha < 1), used in the screen render process."""
message_hook: ContextVar[Callable[[Message], None]] = ContextVar("message_hook")
"""A callable that accepts a message. Used by App.run_test."""
9 changes: 8 additions & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
from ._compose import compose
from ._compositor import CompositorUpdate
from ._context import active_app, active_message_pump
from ._context import message_hook as message_hook_context_var
from ._event_broker import NoHandler, extract_handler_actions
from ._path import _make_path_object_relative
from ._wait import wait_for_idle
Expand Down Expand Up @@ -100,6 +101,7 @@
# Unused & ignored imports are needed for the docs to link to these objects:
from .css.query import WrongType # type: ignore # noqa: F401
from .devtools.client import DevtoolsClient
from .message import Message
from .pilot import Pilot
from .widget import MountError # type: ignore # noqa: F401

Expand Down Expand Up @@ -1060,6 +1062,7 @@ async def run_test(
headless: bool = True,
size: tuple[int, int] | None = (80, 24),
tooltips: bool = False,
message_hook: Callable[[Message], None] | None = None,
) -> AsyncGenerator[Pilot, None]:
"""An asynchronous context manager for testing app.
Expand All @@ -1078,6 +1081,7 @@ async def run_test(
size: Force terminal size to `(WIDTH, HEIGHT)`,
or None to auto-detect.
tooltips: Enable tooltips when testing.
message_hook: An optional callback that will called with every message going through the app.
"""
from .pilot import Pilot

Expand All @@ -1090,6 +1094,8 @@ def on_app_ready() -> None:
app_ready_event.set()

async def run_app(app) -> None:
if message_hook is not None:
message_hook_context_var.set(message_hook)
app._loop = asyncio.get_running_loop()
app._thread_id = threading.get_ident()
await app._process_messages(
Expand Down Expand Up @@ -1824,6 +1830,7 @@ async def _process_messages(
ready_callback: CallbackType | None = None,
headless: bool = False,
terminal_size: tuple[int, int] | None = None,
message_hook: Callable[[Message], None] | None = None,
) -> None:
self._set_active()
active_message_pump.set(self)
Expand Down Expand Up @@ -1978,7 +1985,7 @@ async def take_screenshot() -> None:

async def _on_compose(self) -> None:
try:
widgets = compose(self)
widgets = [*self.screen._nodes, *compose(self)]
except TypeError as error:
raise TypeError(
f"{self!r} compose() method returned an invalid result; {error}"
Expand Down
16 changes: 10 additions & 6 deletions src/textual/message_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,9 @@
from . import Logger, events, log, messages
from ._asyncio import create_task
from ._callback import invoke
from ._context import (
NoActiveAppError,
active_app,
active_message_pump,
prevent_message_types_stack,
)
from ._context import NoActiveAppError, active_app, active_message_pump
from ._context import message_hook as message_hook_context_var
from ._context import prevent_message_types_stack
from ._on import OnNoWidget
from ._time import time
from ._types import CallbackType
Expand Down Expand Up @@ -554,6 +551,13 @@ async def _dispatch_message(self, message: Message) -> None:
if message.no_dispatch:
return

try:
message_hook = message_hook_context_var.get()
except LookupError:
pass
else:
message_hook(message)

with self.prevent(*message._prevent):
# Allow apps to treat events and messages separately
if isinstance(message, Event):
Expand Down
2 changes: 1 addition & 1 deletion src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -3121,7 +3121,7 @@ async def handle_key(self, event: events.Key) -> bool:

async def _on_compose(self) -> None:
try:
widgets = compose(self)
widgets = [*self._nodes, *compose(self)]
except TypeError as error:
raise TypeError(
f"{self!r} compose() method returned an invalid result; {error}"
Expand Down
45 changes: 45 additions & 0 deletions tests/test_widget.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import pytest
from rich.text import Text

from textual import events
from textual._node_list import DuplicateIds
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.css.errors import StyleValueError
from textual.css.query import NoMatches
from textual.geometry import Size
from textual.message import Message
from textual.widget import MountError, PseudoClasses, Widget
from textual.widgets import Label

Expand Down Expand Up @@ -260,3 +262,46 @@ def test_render_str() -> None:
# Text objects are passed unchanged
text = Text("bar")
assert widget.render_str(text) is text


async def test_compose_order() -> None:
from textual.containers import Horizontal
from textual.screen import Screen
from textual.widgets import Select

class MyScreen(Screen):
def on_mount(self) -> None:
self.query_one(Select).value = 1

def compose(self) -> ComposeResult:
yield Horizontal(
Select(((str(n), n) for n in range(10)), id="select"),
id="screen-horizontal",
)

class SelectBugApp(App[None]):
async def on_mount(self):
await self.push_screen(MyScreen(id="my-screen"))
self.query_one(Select)

app = SelectBugApp()
messages: list[Message] = []

async with app.run_test(message_hook=messages.append) as pilot:
await pilot.pause()

mounts = [
message._sender.id
for message in messages
if isinstance(message, events.Mount) and message._sender.id is not None
]

expected = [
"_default", # default screen
"label", # A static in select
"select", # The select
"screen-horizontal", # The horizontal in MyScreen.compose
"my-screen", # THe screen mounted in the app
]

assert mounts == expected

0 comments on commit 0849e6f

Please sign in to comment.