Skip to content

Commit a3e91d4

Browse files
authored
mount error (#3780)
* mount error * changelog
1 parent 29d6f3d commit a3e91d4

File tree

5 files changed

+54
-2
lines changed

5 files changed

+54
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1717
### Added
1818

1919
- Added support for Ctrl+Fn and Ctrl+Shift+Fn keys in urxvt https://github.com/Textualize/textual/pull/3737
20+
- Friendly error messages when trying to mount non-widgets https://github.com/Textualize/textual/pull/3780
2021

2122
## [0.43.2] - 2023-11-29
2223

src/textual/_compose.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,51 @@ def compose(node: App | Widget) -> list[Widget]:
1717
A list of widgets.
1818
"""
1919
_rich_traceback_omit = True
20+
from .widget import MountError, Widget
21+
2022
app = node.app
2123
nodes: list[Widget] = []
2224
compose_stack: list[Widget] = []
2325
composed: list[Widget] = []
2426
app._compose_stacks.append(compose_stack)
2527
app._composed.append(composed)
2628
iter_compose = iter(node.compose())
29+
is_generator = hasattr(iter_compose, "throw")
2730
try:
2831
while True:
2932
try:
3033
child = next(iter_compose)
3134
except StopIteration:
3235
break
36+
37+
if not isinstance(child, Widget):
38+
mount_error = MountError(
39+
f"Can't mount {type(child)}; expected a Widget instance."
40+
)
41+
if is_generator:
42+
iter_compose.throw(mount_error) # type: ignore
43+
else:
44+
raise mount_error from None
45+
46+
try:
47+
child.id
48+
except AttributeError:
49+
mount_error = MountError(
50+
"Widget is missing an 'id' attribute; did you forget to call super().__init__()?"
51+
)
52+
if is_generator:
53+
iter_compose.throw(mount_error) # type: ignore
54+
else:
55+
raise mount_error from None
56+
3357
if composed:
3458
nodes.extend(composed)
3559
composed.clear()
3660
if compose_stack:
3761
try:
3862
compose_stack[-1].compose_add_child(child)
3963
except Exception as error:
40-
if hasattr(iter_compose, "throw"):
64+
if is_generator:
4165
# So the error is raised inside the generator
4266
# This will generate a more sensible traceback for the dev
4367
iter_compose.throw(error) # type: ignore

src/textual/pilot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ async def _post_mouse_events(
328328
# the driver works and emits a click event.
329329
widget_at, _ = app.get_widget_at(*offset)
330330
event = mouse_event_cls(**message_arguments)
331+
# Bypass event processing in App.on_event
331332
app.screen._forward_event(event)
332333
await self.pause()
333334

src/textual/widget.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,6 @@ def mount(
807807
Only one of ``before`` or ``after`` can be provided. If both are
808808
provided a ``MountError`` will be raised.
809809
"""
810-
811810
# Check for duplicate IDs in the incoming widgets
812811
ids_to_mount = [widget.id for widget in widgets if widget.id is not None]
813812
unique_ids = set(ids_to_mount)

tests/test_widget.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,30 @@ def compose(self) -> ComposeResult:
410410
with pytest.raises(NotAContainer):
411411
async with app.run_test():
412412
pass
413+
414+
415+
async def test_mount_error_not_widget():
416+
class NotWidgetApp(App):
417+
def compose(self) -> ComposeResult:
418+
yield {}
419+
420+
app = NotWidgetApp()
421+
with pytest.raises(MountError):
422+
async with app.run_test():
423+
pass
424+
425+
426+
async def test_mount_error_bad_widget():
427+
class DaftWidget(Widget):
428+
def __init__(self):
429+
# intentionally missing super()
430+
pass
431+
432+
class NotWidgetApp(App):
433+
def compose(self) -> ComposeResult:
434+
yield DaftWidget()
435+
436+
app = NotWidgetApp()
437+
with pytest.raises(MountError):
438+
async with app.run_test():
439+
pass

0 commit comments

Comments
 (0)