Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move child acts as noop if trying to move before/after itself #3896

Merged
merged 5 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Fixed

- `Widget.move_child` would break if `before`/`after` is set to the index of the widget in `child` https://github.com/Textualize/textual/issues/1743

### Changed

- Breaking change: `Widget.move_child` parameters `before` and `after` are now keyword-only https://github.com/Textualize/textual/pull/3896

## [0.46.0] - 2023-12-17

### Fixed
Expand Down
28 changes: 24 additions & 4 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,9 +892,30 @@ def mount_all(
await_mount = self.mount(*widgets, before=before, after=after)
return await_mount

@overload
def move_child(
self,
child: int | Widget,
*,
before: int | Widget,
after: None = None,
) -> None:
...

@overload
def move_child(
self,
child: int | Widget,
*,
after: int | Widget,
before: None = None,
) -> None:
...

def move_child(
self,
child: int | Widget,
*,
before: int | Widget | None = None,
after: int | Widget | None = None,
) -> None:
Expand All @@ -920,10 +941,6 @@ def move_child(
elif before is not None and after is not None:
raise WidgetError("Only one of `before` or `after` can be handled.")

# We short-circuit the no-op, otherwise it will error later down the road.
if child is before or child is after:
return

def _to_widget(child: int | Widget, called: str) -> Widget:
"""Ensure a given child reference is a Widget."""
if isinstance(child, int):
Expand All @@ -948,6 +965,9 @@ def _to_widget(child: int | Widget, called: str) -> Widget:
cast("int | Widget", before if after is None else after), "move towards"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overloads above are smart enough to detect issues when calling the function with none of after/before or with both but they're not smart enough to infer that one of the two is not None inside the function body so I couldn't get rid of this cast.

)

if child is target:
return # Nothing to be done.

# At this point we should know what we're moving, and it should be a
# child; where we're moving it to, which should be within the child
# list; and how we're supposed to move it. All that's left is doing
Expand Down
88 changes: 71 additions & 17 deletions tests/test_widget_child_moving.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
async def test_move_child_no_direction() -> None:
"""Test moving a widget in a child list."""
async with App().run_test() as pilot:
child = Widget(Widget())
child = Widget()
await pilot.app.mount(child)
with pytest.raises(WidgetError):
pilot.app.screen.move_child(child)
Expand All @@ -18,7 +18,7 @@ async def test_move_child_no_direction() -> None:
async def test_move_child_both_directions() -> None:
"""Test calling move_child with more than one direction."""
async with App().run_test() as pilot:
child = Widget(Widget())
child = Widget()
await pilot.app.mount(child)
with pytest.raises(WidgetError):
pilot.app.screen.move_child(child, before=1, after=2)
Expand All @@ -27,7 +27,7 @@ async def test_move_child_both_directions() -> None:
async def test_move_child_not_our_child() -> None:
"""Test attempting to move a child that isn't ours."""
async with App().run_test() as pilot:
child = Widget(Widget())
child = Widget()
await pilot.app.mount(child)
with pytest.raises(WidgetError):
pilot.app.screen.move_child(Widget(), before=child)
Expand All @@ -36,28 +36,82 @@ async def test_move_child_not_our_child() -> None:
async def test_move_child_to_outside() -> None:
"""Test attempting to move relative to a widget that isn't a child."""
async with App().run_test() as pilot:
child = Widget(Widget())
child = Widget()
await pilot.app.mount(child)
with pytest.raises(WidgetError):
pilot.app.screen.move_child(child, before=Widget())


async def test_move_child_before_itself() -> None:
"""Test moving a widget before itself."""
@pytest.mark.parametrize(
"reference",
[
"before",
"after",
],
)
async def test_move_child_index_in_relation_to_itself_index(reference: str) -> None:
"""Regression test for https://github.com/Textualize/textual/issues/1743"""

widget = Widget()
child = 0
kwargs = {reference: 0}
async with App().run_test() as pilot:
child = Widget(Widget())
await pilot.app.mount(child)
pilot.app.screen.move_child(child, before=child)


async def test_move_child_after_itself() -> None:
"""Test moving a widget after itself."""
# Regression test for https://github.com/Textualize/textual/issues/1743
await pilot.app.screen.mount(widget)
pilot.app.screen.move_child(child, **kwargs) # Shouldn't raise an error.


@pytest.mark.parametrize(
"reference",
[
"before",
"after",
],
)
async def test_move_child_index_in_relation_to_itself_widget(reference: str) -> None:
"""Regression test for https://github.com/Textualize/textual/issues/1743"""

widget = Widget()
child = 0
kwargs = {reference: widget}
async with App().run_test() as pilot:
child = Widget(Widget())
await pilot.app.mount(child)
pilot.app.screen.move_child(child, after=child)
await pilot.app.screen.mount(widget)
pilot.app.screen.move_child(child, **kwargs) # Shouldn't raise an error.


@pytest.mark.parametrize(
"reference",
[
"before",
"after",
],
)
async def test_move_child_widget_in_relation_to_itself_index(reference: str) -> None:
"""Regression test for https://github.com/Textualize/textual/issues/1743"""

widget = Widget()
child = widget
kwargs = {reference: 0}
async with App().run_test() as pilot:
await pilot.app.screen.mount(widget)
pilot.app.screen.move_child(child, **kwargs) # Shouldn't raise an error.


@pytest.mark.parametrize(
"reference",
[
"before",
"after",
],
)
async def test_move_child_widget_in_relation_to_itself_widget(reference: str) -> None:
"""Regression test for https://github.com/Textualize/textual/issues/1743"""

widget = Widget()
child = widget
kwargs = {reference: widget}
async with App().run_test() as pilot:
await pilot.app.screen.mount(widget)
pilot.app.screen.move_child(child, **kwargs) # Shouldn't raise an error.


async def test_move_past_end_of_child_list() -> None:
Expand Down
Loading