Skip to content

Commit 4e8c10b

Browse files
authored
Merge pull request #1138 from davep/no-self-own
Raise an error if a widget tries to be its own parent
2 parents b09f28d + 0324fb9 commit 4e8c10b

File tree

3 files changed

+31
-12
lines changed

3 files changed

+31
-12
lines changed

src/textual/widget.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ class RenderCache(NamedTuple):
147147
lines: Lines
148148

149149

150+
class WidgetError(Exception):
151+
"""Base widget error."""
152+
153+
154+
class MountError(WidgetError):
155+
"""Error raised when there was a problem with the mount request."""
156+
157+
150158
@rich.repr.auto
151159
class Widget(DOMNode):
152160
"""
@@ -233,6 +241,10 @@ def __init__(
233241
id=id,
234242
classes=self.DEFAULT_CLASSES if classes is None else classes,
235243
)
244+
245+
if self in children:
246+
raise WidgetError("A widget can't be its own parent")
247+
236248
self._add_children(*children)
237249

238250
virtual_size = Reactive(Size(0, 0), layout=True)
@@ -374,9 +386,6 @@ def _get_virtual_dom(self) -> Iterable[Widget]:
374386
if self._scrollbar_corner is not None:
375387
yield self._scrollbar_corner
376388

377-
class MountError(Exception):
378-
"""Error raised when there was a problem with the mount request."""
379-
380389
def _find_mount_point(self, spot: int | str | "Widget") -> tuple["Widget", int]:
381390
"""Attempt to locate the point where the caller wants to mount something.
382391
@@ -387,7 +396,7 @@ def _find_mount_point(self, spot: int | str | "Widget") -> tuple["Widget", int]:
387396
tuple[Widget, int]: The parent and the location in its child list.
388397
389398
Raises:
390-
Widget.MountError: If there was an error finding where to mount a widget.
399+
MountError: If there was an error finding where to mount a widget.
391400
392401
The rules of this method are:
393402
@@ -414,7 +423,7 @@ def _find_mount_point(self, spot: int | str | "Widget") -> tuple["Widget", int]:
414423
# have a parent? There's no way we can use it as a sibling to make
415424
# mounting decisions if it doesn't have a parent.
416425
if spot.parent is None:
417-
raise self.MountError(
426+
raise MountError(
418427
f"Unable to find relative location of {spot!r} because it has no parent"
419428
)
420429

@@ -424,7 +433,7 @@ def _find_mount_point(self, spot: int | str | "Widget") -> tuple["Widget", int]:
424433
try:
425434
return spot.parent, spot.parent.children.index(spot)
426435
except ValueError:
427-
raise self.MountError(f"{spot!r} is not a child of {self!r}") from None
436+
raise MountError(f"{spot!r} is not a child of {self!r}") from None
428437

429438
def mount(
430439
self,
@@ -452,7 +461,7 @@ def mount(
452461

453462
# Saying you want to mount before *and* after something is an error.
454463
if before is not None and after is not None:
455-
raise self.MountError(
464+
raise MountError(
456465
"Only one of `before` or `after` can be handled -- not both"
457466
)
458467

tests/test_widget_mount_point.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from textual.widget import Widget
3+
from textual.widget import Widget, MountError
44

55

66
class Content(Widget):
@@ -36,5 +36,5 @@ def test_find_dom_spot():
3636
# Finally, let's be sure that we get an error if, for some odd reason,
3737
# we go looking for a widget that isn't actually part of the DOM we're
3838
# looking in.
39-
with pytest.raises(Widget.MountError):
39+
with pytest.raises(MountError):
4040
_ = screen._find_mount_point(Widget())

tests/test_widget_mounting.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
import pytest
22

33
from textual.app import App
4+
from textual.widget import Widget, WidgetError, MountError
45
from textual.widgets import Static
56
from textual.css.query import TooManyMatches
67

8+
class SelfOwn(Widget):
9+
"""Test a widget that tries to own itself."""
10+
def __init__(self) -> None:
11+
super().__init__(self)
12+
713
async def test_mount_via_app() -> None:
814
"""Perform mount tests via the app."""
915

1016
# Make a background set of widgets.
1117
widgets = [Static(id=f"starter-{n}") for n in range( 10 )]
1218

19+
async with App().run_test() as pilot:
20+
with pytest.raises(WidgetError):
21+
await pilot.app.mount(SelfOwn())
22+
1323
async with App().run_test() as pilot:
1424
# Mount the first one and make sure it's there.
1525
await pilot.app.mount(widgets[0])
@@ -83,16 +93,16 @@ async def test_mount_via_app() -> None:
8393
async with App().run_test() as pilot:
8494
# Make sure we get told off for trying to before and after.
8595
await pilot.app.mount_all(widgets)
86-
with pytest.raises(Static.MountError):
96+
with pytest.raises(MountError):
8797
await pilot.app.mount(Static(), before=2, after=2)
8898

8999
async with App().run_test() as pilot:
90100
# Make sure we get told off trying to mount relative to something
91101
# that isn't actually in the DOM.
92102
await pilot.app.mount_all(widgets)
93-
with pytest.raises(Static.MountError):
103+
with pytest.raises(MountError):
94104
await pilot.app.mount(Static(), before=Static())
95-
with pytest.raises(Static.MountError):
105+
with pytest.raises(MountError):
96106
await pilot.app.mount(Static(), after=Static())
97107

98108
async with App().run_test() as pilot:

0 commit comments

Comments
 (0)