Skip to content

Commit

Permalink
Add weakref with opt in automatic widget deletion using 'enable_weakr…
Browse files Browse the repository at this point in the history
…eference'.
  • Loading branch information
Alan Fleming committed May 26, 2024
1 parent 4690a5d commit 448c76d
Show file tree
Hide file tree
Showing 4 changed files with 347 additions and 77 deletions.
2 changes: 1 addition & 1 deletion python/ipywidgets/ipywidgets/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from .widget import Widget, CallbackDispatcher, register, widget_serialization
from .widget import Widget, CallbackDispatcher, register, widget_serialization, enable_weakreference, disable_weakreference
from .domwidget import DOMWidget
from .valuewidget import ValueWidget

Expand Down
173 changes: 171 additions & 2 deletions python/ipywidgets/ipywidgets/widgets/tests/test_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@

"""Test Widget."""

import copy
import gc
import inspect
import weakref

import pytest
from IPython.core.interactiveshell import InteractiveShell
from IPython.display import display
from IPython.utils.capture import capture_output

import ipywidgets as ipw

from .. import widget
from ..widget import Widget
from ..widget_button import Button
import copy


def test_no_widget_view():
Expand Down Expand Up @@ -88,4 +92,169 @@ def test_widget_copy():
with pytest.raises(NotImplementedError):
copy.copy(button)
with pytest.raises(NotImplementedError):
copy.deepcopy(button)
copy.deepcopy(button)


def test_widget_open():
button = Button()
model_id = button.model_id
assert model_id in widget._instances
spec = button.get_view_spec()
assert list(spec) == ["version_major", "version_minor", "model_id"]
assert spec["model_id"]
button.close()
assert model_id not in widget._instances
with pytest.raises(RuntimeError, match="Widget is closed"):
button.open()
with pytest.raises(RuntimeError, match="Widget is closed"):
button.get_view_spec()


@pytest.mark.parametrize(
"class_name",
[
"Accordion",
"AppLayout",
"Audio",
"BoundedFloatText",
"BoundedIntText",
"Box",
"Button",
"ButtonStyle",
"Checkbox",
"ColorPicker",
"ColorsInput",
"Combobox",
"Controller",
"CoreWidget",
"DOMWidget",
"DatePicker",
"DatetimePicker",
"Dropdown",
"FileUpload",
"FloatLogSlider",
"FloatProgress",
"FloatRangeSlider",
"FloatSlider",
"FloatText",
"FloatsInput",
"GridBox",
"HBox",
"HTML",
"HTMLMath",
"Image",
"IntProgress",
"IntRangeSlider",
"IntSlider",
"IntText",
"IntsInput",
"Label",
"Layout",
"NaiveDatetimePicker",
"Output",
"Password",
"Play",
"RadioButtons",
"Select",
"SelectMultiple",
"SelectionRangeSlider",
"SelectionSlider",
"SliderStyle",
"Stack",
"Style",
"Tab",
"TagsInput",
"Text",
"Textarea",
"TimePicker",
"ToggleButton",
"ToggleButtons",
"ToggleButtonsStyle",
"TwoByTwoLayout",
"VBox",
"Valid",
"ValueWidget",
"Video",
"Widget",
],
)
@pytest.mark.parametrize("enable_weakref", [True, False])
def test_weakreference(class_name, enable_weakref):
# Ensure the base instance of all widgets can be deleted / garbage collected.
if enable_weakref:
ipw.enable_weakreference()
cls = getattr(ipw, class_name)
if class_name in ['SelectionRangeSlider', 'SelectionSlider']:
kwgs = {"options": [1, 2, 4]}
else:
kwgs = {}
try:
w = cls(**kwgs)
deleted = False
def on_delete():
nonlocal deleted
deleted = True
weakref.finalize(w, on_delete)
# w should be the only strong ref to the widget.
# calling `del` should invoke its immediate deletion calling the `__del__` method.
if not enable_weakref:
w.close()
del w
gc.collect()
assert deleted
finally:
if enable_weakref:
ipw.disable_weakreference()


@pytest.mark.parametrize("weakref_enabled", [True, False])
def test_button_weakreference(weakref_enabled: bool):
try:
click_count = 0
deleted = False

def on_delete():
nonlocal deleted
deleted = True

class TestButton(Button):
def my_click(self, b):
nonlocal click_count
click_count += 1

b = TestButton(description="button")
weakref.finalize(b, on_delete)
b_ref = weakref.ref(b)
assert b in widget._instances.values()

b.on_click(b.my_click)
b.on_click(lambda x: setattr(x, "clicked", True))

b.click()
assert click_count == 1

if weakref_enabled:
ipw.enable_weakreference()
assert b in widget._instances.values(), "Instances not transferred"
ipw.disable_weakreference()
assert b in widget._instances.values(), "Instances not transferred"
ipw.enable_weakreference()
assert b in widget._instances.values(), "Instances not transferred"

b.click()
assert click_count == 2
assert getattr(b, "clicked")

del b
gc.collect()
if weakref_enabled:
assert deleted
else:
assert not deleted
assert b_ref() in widget._instances.values()
b_ref().close()
gc.collect()
assert deleted, "Closing should remove the last strong reference."

finally:
ipw.disable_weakreference()
92 changes: 72 additions & 20 deletions python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,85 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from unittest import TestCase
import gc
import weakref

import pytest
from traitlets import TraitError

import ipywidgets as widgets


class TestBox(TestCase):
def test_box_construction():
box = widgets.Box()
assert box.get_state()["children"] == []

def test_construction(self):
box = widgets.Box()
assert box.get_state()['children'] == []

def test_construction_with_children(self):
html = widgets.HTML('some html')
slider = widgets.IntSlider()
box = widgets.Box([html, slider])
children_state = box.get_state()['children']
assert children_state == [
widgets.widget._widget_to_json(html, None),
widgets.widget._widget_to_json(slider, None),
]
def test_box_construction_with_children():
html = widgets.HTML("some html")
slider = widgets.IntSlider()
box = widgets.Box([html, slider])
children_state = box.get_state()["children"]
assert children_state == [
widgets.widget._widget_to_json(html, None),
widgets.widget._widget_to_json(slider, None),
]

def test_construction_style(self):
box = widgets.Box(box_style='warning')
assert box.get_state()['box_style'] == 'warning'

def test_construction_invalid_style(self):
with self.assertRaises(TraitError):
widgets.Box(box_style='invalid')
def test_box_construction_style():
box = widgets.Box(box_style="warning")
assert box.get_state()["box_style"] == "warning"


def test_construction_invalid_style():
with pytest.raises(TraitError):
widgets.Box(box_style="invalid")


def test_box_validate_mode():
slider = widgets.IntSlider()
closed_button = widgets.Button()
closed_button.close()
with pytest.raises(TraitError, match="Invalid or closed items found.*"):
widgets.Box(
children=[closed_button, slider, "Not a widget"]
)
box = widgets.Box(
children=[closed_button, slider, "Not a widget"],
validate_mode="log_error",
)
assert len (box.children) == 1, "Invalid items should be dropped."
assert slider in box.children

box.validate_mode = "raise"
with pytest.raises(TraitError):
box.children += ("Not a widget", closed_button)


def test_box_gc():
widgets.VBox._active_widgets
widgets.enable_weakreference()
# Test Box gc collected and children lifecycle managed.
try:
deleted = False

class TestButton(widgets.Button):
def my_click(self, b):
pass

button = TestButton(description="button")
button.on_click(button.my_click)

b = widgets.VBox(children=[button])

def on_delete():
nonlocal deleted
deleted = True

weakref.finalize(b, on_delete)
del b
gc.collect()
assert deleted
widgets.VBox._active_widgets
finally:
widgets.disable_weakreference()
Loading

0 comments on commit 448c76d

Please sign in to comment.