Skip to content

Commit

Permalink
Added a closed property to the Widget class. Modified __repr__ to i…
Browse files Browse the repository at this point in the history
…dentify closed widgets.

Added a new class `Children` for the children trait of Box optimised for checking widgets
and quietly dropping widgets that are closed, and objects that aren't widgets. Widgets in Box.children
 are also removed when the widget is closed.
 Added tests test_gc_box & test_gc_box_advanced
  • Loading branch information
Alan Fleming committed Mar 11, 2024
1 parent 24ad054 commit 2bcbed3
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 5 deletions.
57 changes: 57 additions & 0 deletions python/ipywidgets/ipywidgets/widgets/tests/test_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .. import widget
from ..widget import Widget
from ..widget_button import Button
from ..widget_box import VBox
import copy

import ipywidgets as ipw
Expand Down Expand Up @@ -148,3 +149,59 @@ def on_delete():
del b
gc.collect()
assert deleted


def test_gc_box():
# Test Box gc collected and children lifecycle managed.
deleted = False
b = VBox(children=[Button(description='button')])

def on_delete():
nonlocal deleted
deleted = True

weakref.finalize(b, on_delete)
del b
gc.collect()
assert deleted

def test_gc_box_advanced():
# A more advanced test for:
# 1. A child widget is removed from the children when it is closed
# 2. The children are discarded when the widget is closed.

deleted = False

b = VBox(
children=[
Button(description="b0"),
Button(description="b1"),
Button(description="b2"),
]
)

def on_delete():
nonlocal deleted
deleted = True

weakref.finalize(b, on_delete)

ids = [model_id for w in b.children if (model_id:=w.model_id) in widget._instances]
assert len(ids) == 3, 'Not all button comms were registered.'

# keep a strong ref to `b1`
b1 = b.children[1]

# When a widget is closed it should be removed from the box.children.
b.children[0].close()
assert len(b.children) == 2, "b0 not removed."
# assert 'b0' in deleted

# When the ref to box is removed it should be deleted.
del b
assert deleted, "`b` should have been the only strong ref to the box."
# assert not b.children, '`children` should be removed when the widget is closed.'
assert not b1.closed, 'A removed widget should remain alive.'

# b2 shouldn't have any strong references so should be deleted.
assert ids[2] not in widget._instances, 'b2 should have been auto deleted.'
14 changes: 13 additions & 1 deletion python/ipywidgets/ipywidgets/widgets/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def _widget_to_json(x, obj):
elif isinstance(x, (list, tuple)):
return [_widget_to_json(v, obj) for v in x]
elif isinstance(x, Widget):
if x.closed:
msg = f"Widget is {x!r}"
raise RuntimeError(msg)
return "IPY_MODEL_" + x.model_id
else:
return x
Expand Down Expand Up @@ -557,6 +560,14 @@ def model_id(self):
If a Comm doesn't exist yet, a Comm will be created automagically."""
return getattr(self.comm, "comm_id", None)

@property
def closed(self) -> bool:
"""Returns True when comms is closed.
There is no possibility to re-open once it is closed."""
# If comm is None it indicates the comm is closed and the widget is closed.
return self._trait_values.get('comm', False) is None

#-------------------------------------------------------------------------
# Methods
Expand Down Expand Up @@ -706,7 +717,8 @@ def notify_change(self, change):
super().notify_change(change)

def __repr__(self):
return self._gen_repr_from_keys(self._repr_keys())
rep = self._gen_repr_from_keys(self._repr_keys())
return 'closed: ' + rep if self.closed else rep

#-------------------------------------------------------------------------
# Support methods
Expand Down
40 changes: 36 additions & 4 deletions python/ipywidgets/ipywidgets/widgets/widget_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
group other widgets together and control their
relative layouts.
"""
import weakref

from .widget import register, widget_serialization, Widget
from .domwidget import DOMWidget
from .widget_core import CoreWidget
from .docutils import doc_subst
from .trait_types import TypedTuple
from traitlets import Unicode, CaselessStrEnum, Instance
from traitlets import Unicode, CaselessStrEnum, TraitType, observe


_doc_snippets = {}
Expand All @@ -27,6 +27,12 @@
which applies no pre-defined style.
"""

class Children(TraitType[tuple[Widget],tuple[Widget]]):
default_value = ()

def validate(self, obj:'Box', value):
return tuple(v for v in value if isinstance(v, Widget) and not v.closed)


@register
@doc_subst(_doc_snippets)
Expand All @@ -52,17 +58,43 @@ class Box(DOMWidget, CoreWidget):
# Child widgets in the container.
# Using a tuple here to force reassignment to update the list.
# When a proper notifying-list trait exists, use that instead.
children = TypedTuple(trait=Instance(Widget), help="List of widget children").tag(
children = Children(help="List of widget children").tag(
sync=True, **widget_serialization)

box_style = CaselessStrEnum(
values=['success', 'info', 'warning', 'danger', ''], default_value='',
help="""Use a predefined styling for the box.""").tag(sync=True)

def __init__(self, children=(), **kwargs):
kwargs['children'] = children
if children:
kwargs['children'] = children
super().__init__(**kwargs)

@observe('children')
def _box_observe_children(self, change):
# Monitor widgets for when the comm is closed.
handler = getattr(self, "_widget_children_comm_handler", None)
if not handler:
ref = weakref.ref(self)
def handler(change):
self_ = ref()
if self_ and change['owner']:
# Re-validation will discard all closed widgets.
self_.children = self_.children

self._widget_children_comm_handler = handler
if change['new']:
w:Widget
for w in set(change['new']).difference(change['old'] or ()):
w.observe(handler, names='comm')
if change['old']:
for w in set(change['old']).difference(change['new']):
try:
w.unobserve(handler, names='comm')
except ValueError:
pass


@register
@doc_subst(_doc_snippets)
class VBox(Box):
Expand Down

0 comments on commit 2bcbed3

Please sign in to comment.