diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index 9a2ebc31..319cdbb0 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -341,8 +341,12 @@ def remove(self, value: Widget | str) -> None: super().remove(value) # type: ignore def __delattr__(self, name: str) -> None: - """Delete a widget by name.""" - self.remove(name) + """Delete a widget by name, or a plain attribute if no widget matches.""" + for widget in self._list: + if name == widget.name: + self.remove(widget) + return + object.__delattr__(self, name) def __delitem__(self, key: int | slice) -> None: """Delete a widget by integer or slice index.""" diff --git a/tests/test_container.py b/tests/test_container.py index 15a85880..19dbeebb 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -117,6 +117,24 @@ def test_delete_widget(): container.index(a) +def test_delete_non_widget_attribute(): + """Deleting a plain attribute does not go through widget removal.""" + a = widgets.Label(name="a") + container = widgets.Container(widgets=[a]) + container.x = 5 + del container.x + assert not hasattr(container, "x") + + # a widget that was already removed can still be dereferenced + container.remove(a) + container.a_ref = a + del container.a_ref + + # a name that is neither a widget nor an attribute still raises AttributeError + with pytest.raises(AttributeError): + del container.nonexistent + + def test_reset_choice_recursion(): """Test that reset_choices recursion works for multiple types of widgets.""" x = 0