Skip to content

Commit

Permalink
mount error (#3780)
Browse files Browse the repository at this point in the history
* mount error

* changelog
  • Loading branch information
willmcgugan authored Nov 29, 2023
1 parent 29d6f3d commit a3e91d4
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

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

## [0.43.2] - 2023-11-29

Expand Down
26 changes: 25 additions & 1 deletion src/textual/_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,51 @@ def compose(node: App | Widget) -> list[Widget]:
A list of widgets.
"""
_rich_traceback_omit = True
from .widget import MountError, Widget

app = node.app
nodes: list[Widget] = []
compose_stack: list[Widget] = []
composed: list[Widget] = []
app._compose_stacks.append(compose_stack)
app._composed.append(composed)
iter_compose = iter(node.compose())
is_generator = hasattr(iter_compose, "throw")
try:
while True:
try:
child = next(iter_compose)
except StopIteration:
break

if not isinstance(child, Widget):
mount_error = MountError(
f"Can't mount {type(child)}; expected a Widget instance."
)
if is_generator:
iter_compose.throw(mount_error) # type: ignore
else:
raise mount_error from None

try:
child.id
except AttributeError:
mount_error = MountError(
"Widget is missing an 'id' attribute; did you forget to call super().__init__()?"
)
if is_generator:
iter_compose.throw(mount_error) # type: ignore
else:
raise mount_error from None

if composed:
nodes.extend(composed)
composed.clear()
if compose_stack:
try:
compose_stack[-1].compose_add_child(child)
except Exception as error:
if hasattr(iter_compose, "throw"):
if is_generator:
# So the error is raised inside the generator
# This will generate a more sensible traceback for the dev
iter_compose.throw(error) # type: ignore
Expand Down
1 change: 1 addition & 0 deletions src/textual/pilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ async def _post_mouse_events(
# the driver works and emits a click event.
widget_at, _ = app.get_widget_at(*offset)
event = mouse_event_cls(**message_arguments)
# Bypass event processing in App.on_event
app.screen._forward_event(event)
await self.pause()

Expand Down
1 change: 0 additions & 1 deletion src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,6 @@ def mount(
Only one of ``before`` or ``after`` can be provided. If both are
provided a ``MountError`` will be raised.
"""

# Check for duplicate IDs in the incoming widgets
ids_to_mount = [widget.id for widget in widgets if widget.id is not None]
unique_ids = set(ids_to_mount)
Expand Down
27 changes: 27 additions & 0 deletions tests/test_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,30 @@ def compose(self) -> ComposeResult:
with pytest.raises(NotAContainer):
async with app.run_test():
pass


async def test_mount_error_not_widget():
class NotWidgetApp(App):
def compose(self) -> ComposeResult:
yield {}

app = NotWidgetApp()
with pytest.raises(MountError):
async with app.run_test():
pass


async def test_mount_error_bad_widget():
class DaftWidget(Widget):
def __init__(self):
# intentionally missing super()
pass

class NotWidgetApp(App):
def compose(self) -> ComposeResult:
yield DaftWidget()

app = NotWidgetApp()
with pytest.raises(MountError):
async with app.run_test():
pass

0 comments on commit a3e91d4

Please sign in to comment.