From 7b393e08a8b98a22ae746da54704959554d9271f Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 10 Mar 2024 09:50:16 +1100 Subject: [PATCH 01/38] Use weakref to replace strong links between widgets and comm --- .gitignore | 1 + packages/controls/src/widget_link.ts | 3 + .../ipywidgets/widgets/tests/test_widget.py | 61 +++++++++++++++- .../ipywidgets/ipywidgets/widgets/widget.py | 72 ++++++++++--------- .../ipywidgets/widgets/widget_button.py | 5 +- .../ipywidgets/widgets/widget_link.py | 31 ++++---- .../ipywidgets/widgets/widget_string.py | 4 +- 7 files changed, 128 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index de6892e769..543bbfef96 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ ui-tests/playwright-report **/lite/.cache **/*.doit.* **/docs/typedoc/ +.yarn/* \ No newline at end of file diff --git a/packages/controls/src/widget_link.ts b/packages/controls/src/widget_link.ts index 2b90330e41..7de1904898 100644 --- a/packages/controls/src/widget_link.ts +++ b/packages/controls/src/widget_link.ts @@ -85,9 +85,12 @@ export class DirectionalLinkModel extends CoreWidgetModel { undefined ); this.stopListening(this.sourceModel, 'destroy', undefined); + this.set('source', [null, '']); } if (this.targetModel) { this.stopListening(this.targetModel, 'destroy', undefined); + this.set('target', [null, '']); + this.save_changes(); } } diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index c5aa36048a..47ae9a0337 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -4,6 +4,8 @@ """Test Widget.""" import inspect +import weakref +import gc import pytest from IPython.core.interactiveshell import InteractiveShell @@ -15,6 +17,7 @@ from ..widget_button import Button import copy +import ipywidgets as ipw def test_no_widget_view(): # ensure IPython shell is instantiated @@ -88,4 +91,60 @@ def test_widget_copy(): with pytest.raises(NotImplementedError): copy.copy(button) with pytest.raises(NotImplementedError): - copy.deepcopy(button) \ No newline at end of file + copy.deepcopy(button) + + +def test_gc(): + # Ensure the base instance of all widgets can be deleted / garbage collected. + classes = {} + for name, obj in ipw.__dict__.items(): + try: + if issubclass(obj, ipw.Widget): + classes[name] = obj + except Exception: + pass + assert classes, "No Widget classes were found!" + added = set() + collected = set() + objs = weakref.WeakSet() + options = ({}, {"options": [1, 2, 4]}, {"n_rows": 1}, {"options": ["A"]}) + for n, obj in classes.items(): + w = None + for kw in options: + try: + w = obj(**kw) + w.comm + added.add(n) + break + except Exception: + pass + if w: + def on_delete(name=n): + collected.add(name) + + weakref.finalize(w, on_delete) + objs.add(w) + # w should be the only strong ref to the widget. + # calling `del` should invoke its immediate deletion calling the `__del__` method. + del w + assert added, "No widgets were tested!" + gc.collect() + diff = added.difference(collected) + assert not diff, f"Widgets not garbage collected: {diff}" + + +def test_gc_button(): + deleted = False + b = Button() + b.on_click(lambda x: setattr(b, "clicked", True)) + + def on_delete(): + nonlocal deleted + deleted = True + + b.click() + assert getattr(b, "clicked") + weakref.finalize(b, on_delete) + del b + gc.collect() + assert deleted diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 2dc674097d..e65de82a7c 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -6,13 +6,13 @@ in the Jupyter notebook front-end. """ import os -import sys import typing +import weakref from contextlib import contextmanager from collections.abc import Iterable from IPython import get_ipython from traitlets import ( - Any, HasTraits, Unicode, Dict, Instance, List, Int, Set, Bytes, observe, default, Container, + Any, HasTraits, Unicode, Dict, Instance, List, Int, Set, observe, default, Container, Undefined) from json import loads as jsonloads, dumps as jsondumps from .. import comm @@ -41,9 +41,9 @@ def envset(name, default): PROTOCOL_VERSION_MAJOR = __protocol_version__.split('.')[0] CONTROL_PROTOCOL_VERSION_MAJOR = __control_protocol_version__.split('.')[0] JUPYTER_WIDGETS_ECHO = envset('JUPYTER_WIDGETS_ECHO', default=True) -# we keep a strong reference for every widget created, for a discussion on using weak references see: +# for a discussion on using weak references see: # https://github.com/jupyter-widgets/ipywidgets/issues/1345 -_instances : typing.MutableMapping[str, "Widget"] = {} +_instances : typing.MutableMapping[str, "Widget"] = weakref.WeakValueDictionary() def _widget_to_json(x, obj): if isinstance(x, dict): @@ -461,7 +461,7 @@ def _get_embed_state(self, drop_defaults=False): return state def get_view_spec(self): - return dict(version_major=2, version_minor=0, model_id=self._model_id) + return dict(version_major=2, version_minor=0, model_id=self.model_id) #------------------------------------------------------------------------- # Traits @@ -499,11 +499,12 @@ def _default_keys(self): #------------------------------------------------------------------------- def __init__(self, **kwargs): """Public constructor""" - self._model_id = kwargs.pop('model_id', None) + if 'model_id' in kwargs: + self.comm = self._create_comm(kwargs.pop('model_id')) super().__init__(**kwargs) + self.open() Widget._call_widget_constructed(self) - self.open() def __copy__(self): raise NotImplementedError("Widgets cannot be copied; custom implementation required") @@ -521,51 +522,56 @@ def __del__(self): def open(self): """Open a comm to the frontend if one isn't already open.""" - if self.comm is None: - state, buffer_paths, buffers = _remove_buffers(self.get_state()) - - args = dict(target_name='jupyter.widget', - data={'state': state, 'buffer_paths': buffer_paths}, - buffers=buffers, - metadata={'version': __protocol_version__} - ) - if self._model_id is not None: - args['comm_id'] = self._model_id - - self.comm = comm.create_comm(**args) + self.comm + + def _create_comm(self, comm_id=None): + """Open a new comm to the frontend.""" + state, buffer_paths, buffers = _remove_buffers(self.get_state()) + self.comm = comm_ = comm.create_comm( + target_name="jupyter.widget", + data={"state": state, "buffer_paths": buffer_paths}, + buffers=buffers, + metadata={"version": __protocol_version__}, + comm_id=comm_id, + ) + return comm_ + + @default('comm') + def _default_comm(self): + return self._create_comm() @observe('comm') def _comm_changed(self, change): """Called when the comm is changed.""" - if change['new'] is None: - return - self._model_id = self.model_id + if change['old']: + change['old'].on_msg(None) + change['old'].close() + _instances.pop(change['old'].comm_id, None) + if change['new']: + _instances[change['new'].comm_id] = self + ref = weakref.ref(self) + change['new'].on_msg(lambda msg: ref()._handle_msg(msg)) - self.comm.on_msg(self._handle_msg) - _instances[self.model_id] = self @property def model_id(self): """Gets the model id of this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" - return self.comm.comm_id + return getattr(self.comm, "comm_id", None) #------------------------------------------------------------------------- # Methods #------------------------------------------------------------------------- def close(self): - """Close method. + """Permanently close the widget. - Closes the underlying comm. + Closes the underlying comm and discards trait values. When the comm is closed, all of the widget views are automatically removed from the front-end.""" - if self.comm is not None: - _instances.pop(self.model_id, None) - self.comm.close() - self.comm = None - self._repr_mimebundle_ = None + self.comm = None + self._repr_mimebundle_ = None def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end, if it exists. @@ -815,7 +821,7 @@ def _repr_mimebundle_(self, **kwargs): data['application/vnd.jupyter.widget-view+json'] = { 'version_major': 2, 'version_minor': 0, - 'model_id': self._model_id + 'model_id': self.model_id } return data diff --git a/python/ipywidgets/ipywidgets/widgets/widget_button.py b/python/ipywidgets/ipywidgets/widgets/widget_button.py index 30c75d7dfa..86d4df0817 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_button.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_button.py @@ -14,9 +14,9 @@ from .widget_style import Style from .trait_types import Color, InstanceDict +import weakref from traitlets import Unicode, Bool, CaselessStrEnum, Instance, validate, default - @register class ButtonStyle(Style, CoreWidget): """Button style widget.""" @@ -63,7 +63,8 @@ class Button(DOMWidget, CoreWidget): def __init__(self, **kwargs): super().__init__(**kwargs) self._click_handlers = CallbackDispatcher() - self.on_msg(self._handle_button_msg) + ref = weakref.ref(self) + self.on_msg(lambda msg: ref()._handle_button_msg(msg)) @validate('icon') def _validate_icon(self, proposal): diff --git a/python/ipywidgets/ipywidgets/widgets/widget_link.py b/python/ipywidgets/ipywidgets/widgets/widget_link.py index 3b6698c084..63b20d3844 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_link.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_link.py @@ -9,7 +9,7 @@ from .widget import Widget, register, widget_serialization from .widget_core import CoreWidget -from traitlets import Unicode, Tuple, Instance, TraitError +from traitlets import Unicode, Tuple, Instance class WidgetTraitTuple(Tuple): @@ -18,7 +18,7 @@ class WidgetTraitTuple(Tuple): info_text = "A (Widget, 'trait_name') pair" def __init__(self, **kwargs): - super().__init__(Instance(Widget), Unicode(), **kwargs) + super().__init__(Instance(Widget, allow_none=True), Unicode(), **kwargs) if "default_value" not in kwargs and not kwargs.get("allow_none", False): # This is to keep consistent behavior for spec generation between traitlets 4 and 5 # Having a default empty container is explicitly not allowed in traitlets 5 when @@ -29,14 +29,18 @@ def __init__(self, **kwargs): def validate_elements(self, obj, value): value = super().validate_elements(obj, value) widget, trait_name = value + if not widget: + obj.close() + return value trait = widget.traits().get(trait_name) - trait_repr = "{}.{}".format(widget.__class__.__name__, trait_name) # Can't raise TraitError because the parent will swallow the message # and throw it away in a new, less informative TraitError if trait is None: - raise TypeError("No such trait: %s" % trait_repr) + msg = f"No such trait: {widget.__class__.__name__}, {trait_name})" + raise TypeError(msg) elif not trait.metadata.get('sync'): - raise TypeError("%s cannot be synced" % trait_repr) + msg = f"Cannot sync: {widget.__class__.__name__}, {trait_name})" + raise TypeError(msg) return value @@ -47,16 +51,19 @@ class Link(CoreWidget): source: a (Widget, 'trait_name') tuple for the source trait target: a (Widget, 'trait_name') tuple that should be updated """ - + # maintain a set of links to keep them alive + _all_links = set() _model_name = Unicode('LinkModel').tag(sync=True) target = WidgetTraitTuple(help="The target (widget, 'trait_name') pair").tag(sync=True, **widget_serialization) source = WidgetTraitTuple(help="The source (widget, 'trait_name') pair").tag(sync=True, **widget_serialization) - - def __init__(self, source, target, **kwargs): - kwargs['source'] = source - kwargs['target'] = target - super().__init__(**kwargs) - + + def __init__(self, source: tuple[Widget, str], target: tuple[Widget, str], **kwargs): + super().__init__(source=source, target=target, **kwargs) + self._all_links.add(self) + + def close(self): + self._all_links.discard(self) + super().close() # for compatibility with traitlet links def unlink(self): self.close() diff --git a/python/ipywidgets/ipywidgets/widgets/widget_string.py b/python/ipywidgets/ipywidgets/widgets/widget_string.py index e6f36bd8cc..3be71289e8 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_string.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_string.py @@ -13,6 +13,7 @@ from .trait_types import Color, InstanceDict, TypedTuple from .utils import deprecation from traitlets import Unicode, Bool, Int +import weakref class _StringStyle(DescriptionStyle, CoreWidget): @@ -117,7 +118,8 @@ class Text(_String): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._submission_callbacks = CallbackDispatcher() - self.on_msg(self._handle_string_msg) + ref = weakref.ref(self) + self.on_msg(lambda msg: ref()._handle_string_msg(msg)) def _handle_string_msg(self, _, content, buffers): """Handle a msg from the front-end. From dca56e3bfad987b991c98b91abe70150512399df Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 10 Mar 2024 10:51:03 +1100 Subject: [PATCH 02/38] Replaced _show_traceback decorator with a method --- .../ipywidgets/ipywidgets/widgets/widget.py | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index e65de82a7c..155d3386de 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -215,18 +215,6 @@ def register_callback(self, callback, remove=False): elif not remove and callback not in self.callbacks: self.callbacks.append(callback) -def _show_traceback(method): - """decorator for showing tracebacks""" - def m(self, *args, **kwargs): - try: - return(method(self, *args, **kwargs)) - except Exception as e: - ip = get_ipython() - if ip is None: - self.log.warning("Exception in widget method %s: %s", method, e, exc_info=True) - else: - ip.showtraceback() - return m class WidgetRegistry: @@ -549,8 +537,18 @@ def _comm_changed(self, change): _instances.pop(change['old'].comm_id, None) if change['new']: _instances[change['new'].comm_id] = self + + # prevent memory leaks by using a weak reference to the widget. ref = weakref.ref(self) - change['new'].on_msg(lambda msg: ref()._handle_msg(msg)) + def _handle_msg(msg): + widget = ref() + if widget: + try: + widget._handle_msg(msg) + except Exception as e: + widget._show_traceback(_handle_msg, e) + + change['new'].on_msg(_handle_msg) @property @@ -765,7 +763,6 @@ def _should_send_property(self, key, value): return True # Event handlers - @_show_traceback def _handle_msg(self, msg): """Called when a msg is received from the front-end""" data = msg['content']['data'] @@ -791,6 +788,14 @@ def _handle_msg(self, msg): else: self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) + def _show_traceback(self, method, e:Exception): + ip = get_ipython() + if ip is None: + self.log.warning("Exception in widget method %s: %s", method, e, exc_info=True) + else: + ip.showtraceback() + + def _handle_custom_msg(self, content, buffers): """Called when a custom msg is received.""" self._msg_callbacks(self, content, buffers) From 871ba61e0b019aa41c990b3ebf570479fd1d2770 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Sun, 10 Mar 2024 13:56:04 +1100 Subject: [PATCH 03/38] Fix signature of callback lamdas --- python/ipywidgets/ipywidgets/widgets/widget_button.py | 2 +- python/ipywidgets/ipywidgets/widgets/widget_string.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget_button.py b/python/ipywidgets/ipywidgets/widgets/widget_button.py index 86d4df0817..a1f42d1e39 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_button.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_button.py @@ -64,7 +64,7 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self._click_handlers = CallbackDispatcher() ref = weakref.ref(self) - self.on_msg(lambda msg: ref()._handle_button_msg(msg)) + self.on_msg(lambda w, c, b: ref()._handle_button_msg(w, c, b)) @validate('icon') def _validate_icon(self, proposal): diff --git a/python/ipywidgets/ipywidgets/widgets/widget_string.py b/python/ipywidgets/ipywidgets/widgets/widget_string.py index 3be71289e8..cfe3e4c9ea 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_string.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_string.py @@ -119,7 +119,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._submission_callbacks = CallbackDispatcher() ref = weakref.ref(self) - self.on_msg(lambda msg: ref()._handle_string_msg(msg)) + self.on_msg(lambda w, c, b: ref()._handle_string_msg(w, c, b)) def _handle_string_msg(self, _, content, buffers): """Handle a msg from the front-end. From 8f22131c8978303859eff46008472fc46b01f469 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Sun, 10 Mar 2024 15:02:42 +1100 Subject: [PATCH 04/38] revise close doc --- python/ipywidgets/ipywidgets/widgets/widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 155d3386de..8160e960cb 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -565,7 +565,7 @@ def model_id(self): def close(self): """Permanently close the widget. - Closes the underlying comm and discards trait values. + Closes the underlying comm. When the comm is closed, all of the widget views are automatically removed from the front-end.""" self.comm = None From ae6df72786de963d8d982da3e5dbdec86a335765 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Sun, 10 Mar 2024 20:00:21 +1100 Subject: [PATCH 05/38] change so Widget.notify_change doesn't create comm --- python/ipywidgets/ipywidgets/widgets/widget.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 8160e960cb..0ea6348be1 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -697,9 +697,10 @@ def notify_change(self, change): # Send the state to the frontend before the user-registered callbacks # are called. name = change['name'] - if self.comm is not None and getattr(self.comm, 'kernel', True) is not None: + comm = self._trait_values.get('comm') + if comm and getattr(comm, 'kernel', None): # Make sure this isn't information that the front-end just sent us. - if name in self.keys and self._should_send_property(name, getattr(self, name)): + if name in self.keys and self._should_send_property(name, change['new']): # Send new state to front-end self.send_state(key=name) super().notify_change(change) From 200edeefa3082d5307700ffccb70f66a0464f48a Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Mon, 11 Mar 2024 10:11:38 +1100 Subject: [PATCH 06/38] Fix reject 'Control comm was closed too early' when comm closed as expected. --- packages/base-manager/src/manager-base.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index 4953767894..9ccddcc12a 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -413,7 +413,10 @@ export abstract class ManagerBase implements IWidgetManager { resolve(null); }); - initComm.on_close(() => reject('Control comm was closed too early')); + initComm.on_close(() => { + if (data.method !== 'update_states') + reject('Control comm was closed too early'); + }); // Send a states request msg initComm.send({ method: 'request_states' }, {}); From 24ad054f740d18409361f1b43bdf8f09f5246009 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Mon, 11 Mar 2024 10:29:41 +1100 Subject: [PATCH 07/38] Fix _show_traceback in _comm_changed --- python/ipywidgets/ipywidgets/widgets/widget.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 0ea6348be1..c96ba3e66e 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -538,15 +538,15 @@ def _comm_changed(self, change): if change['new']: _instances[change['new'].comm_id] = self - # prevent memory leaks by using a weak reference to the widget. + # prevent memory leaks by using a weak reference to self. ref = weakref.ref(self) def _handle_msg(msg): - widget = ref() - if widget: + self_ = ref() + if self_ is not None: try: - widget._handle_msg(msg) + self_._handle_msg(msg) except Exception as e: - widget._show_traceback(_handle_msg, e) + self_._show_traceback(self_._handle_msg, e) change['new'].on_msg(_handle_msg) From 2bcbed3930fead9253d049ec7aa3268fb06ebe9c Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Mon, 11 Mar 2024 16:54:31 +1100 Subject: [PATCH 08/38] Added a `closed` property to the Widget class. Modified __repr__ to identify 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 --- .../ipywidgets/widgets/tests/test_widget.py | 57 +++++++++++++++++++ .../ipywidgets/ipywidgets/widgets/widget.py | 14 ++++- .../ipywidgets/widgets/widget_box.py | 40 +++++++++++-- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index 47ae9a0337..f859cd67b2 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -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 @@ -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.' diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index c96ba3e66e..92664a6979 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -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 @@ -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 @@ -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 diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index 740e54cb1a..d3da00366f 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -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 = {} @@ -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) @@ -52,7 +58,7 @@ 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( @@ -60,9 +66,35 @@ class Box(DOMWidget, CoreWidget): 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): From d3e5e29c1982be2f7e655d6ffca32f3643ff3732 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Mon, 11 Mar 2024 19:35:12 +1100 Subject: [PATCH 09/38] Make compatible with python3.8 --- .../ipywidgets/widgets/widget_box.py | 18 +++++++++++++----- .../ipywidgets/widgets/widget_link.py | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index d3da00366f..bb5570145f 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -14,7 +14,7 @@ from .widget_core import CoreWidget from .docutils import doc_subst from traitlets import Unicode, CaselessStrEnum, TraitType, observe - +import sys _doc_snippets = {} _doc_snippets['box_params'] = """ @@ -27,11 +27,19 @@ which applies no pre-defined style. """ -class Children(TraitType[tuple[Widget],tuple[Widget]]): - default_value = () +# TODO: remove once 3.8 support is dropped. +if sys.version_info < (3, 9): + class Children(TraitType): + default_value = () + + def validate(self, obj:'Box', value): + return tuple(v for v in value if isinstance(v, Widget) and not v.closed) +else: + 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) + def validate(self, obj:'Box', value): + return tuple(v for v in value if isinstance(v, Widget) and not v.closed) @register diff --git a/python/ipywidgets/ipywidgets/widgets/widget_link.py b/python/ipywidgets/ipywidgets/widgets/widget_link.py index 63b20d3844..5916c1c454 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_link.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_link.py @@ -57,7 +57,7 @@ class Link(CoreWidget): target = WidgetTraitTuple(help="The target (widget, 'trait_name') pair").tag(sync=True, **widget_serialization) source = WidgetTraitTuple(help="The source (widget, 'trait_name') pair").tag(sync=True, **widget_serialization) - def __init__(self, source: tuple[Widget, str], target: tuple[Widget, str], **kwargs): + def __init__(self, source, target, **kwargs): super().__init__(source=source, target=target, **kwargs) self._all_links.add(self) From 6877265470262a313edcfd6f4bbbcb51352175ec Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 11 Mar 2024 21:07:37 +1100 Subject: [PATCH 10/38] Modified Widget.open to raise a runtime error if the widget is closed. --- .../ipywidgets/widgets/tests/test_widget.py | 12 +++++++++++- python/ipywidgets/ipywidgets/widgets/widget.py | 6 +++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index f859cd67b2..7457a00396 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -95,6 +95,17 @@ def test_widget_copy(): copy.deepcopy(button) +def test_widget_open(): + button = Button() + assert not button.closed + model_id = button.model_id + assert model_id in widget._instances + button.close() + assert model_id not in widget._instances + with pytest.raises(RuntimeError): + button.open() + + def test_gc(): # Ensure the base instance of all widgets can be deleted / garbage collected. classes = {} @@ -195,7 +206,6 @@ def on_delete(): # 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 diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 92664a6979..8bba4bbd14 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -513,7 +513,11 @@ def __del__(self): def open(self): """Open a comm to the frontend if one isn't already open.""" - self.comm + # Accessing comm will load a default if it isn't already open. + if self.comm is None: + # None indicates the widget has been closed and shall not be opened. + msg = f"This widget is {self!r}." + raise RuntimeError(msg) def _create_comm(self, comm_id=None): """Open a new comm to the frontend.""" From f82c35ca4c5de9028cf261f2813592222c5c183b Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Tue, 12 Mar 2024 22:42:03 +1100 Subject: [PATCH 11/38] Removed the recently added `closed` property, instead noting that _repr_mimebundle_ is set to None when closed, this reduces the risk of name clash with subclasses. Changed Children to allow any object that has the method `_repr_mimebundle_`. Moved box related tests to test_widget_box. Added `close` method to Box to ensure discarded children are un-observed. --- .../ipywidgets/widgets/tests/test_widget.py | 56 --------------- .../widgets/tests/test_widget_box.py | 72 +++++++++++++++++++ .../ipywidgets/ipywidgets/widgets/widget.py | 15 ++-- .../ipywidgets/widgets/widget_box.py | 42 +++++------ 4 files changed, 98 insertions(+), 87 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index 7457a00396..6b5cd6854d 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -97,7 +97,6 @@ def test_widget_copy(): def test_widget_open(): button = Button() - assert not button.closed model_id = button.model_id assert model_id in widget._instances button.close() @@ -160,58 +159,3 @@ 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." - - # 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.' diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py index 551f68dcc4..66f85bf183 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py @@ -1,6 +1,9 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +import gc +import weakref + from unittest import TestCase from traitlets import TraitError @@ -31,3 +34,72 @@ def test_construction_style(self): def test_construction_invalid_style(self): with self.assertRaises(TraitError): widgets.Box(box_style='invalid') + + +def test_box_gc(): + # Test Box gc collected and children lifecycle managed. + deleted = False + b = widgets.VBox(children=[widgets.Button(description='button')]) + + def on_delete(): + nonlocal deleted + deleted = True + + weakref.finalize(b, on_delete) + del b + gc.collect() + assert deleted + +def test_box_child_closed_observe_childen(): + + b1 = widgets.Button(description='button') + b = widgets.VBox(children=[b1], observe_children=True) + b1.close() + assert b1 not in b.children + +def test_box_child_closed_not_observe_childen(): + + b1 = widgets.Button(description='button') + b = widgets.GridBox(children=[b1], observe_children=False) + b1.close() + assert b1 in b.children + +def test_box_gc_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 = widgets.VBox( + children=[ + widgets.Button(description="b0"), + widgets.Button(description="b1"), + widgets.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 widgets.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." + + # 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 b1.comm, 'A removed widget should remain alive.' + + # b2 shouldn't have any strong references so should be deleted. + assert ids[2] not in widgets.widget._instances, 'b2 should have been auto deleted.' diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 8bba4bbd14..b792642522 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -51,10 +51,12 @@ 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: + if not x._repr_mimebundle_: msg = f"Widget is {x!r}" raise RuntimeError(msg) return "IPY_MODEL_" + x.model_id + elif hasattr(x, '_repr_mimebundle_'): + return x._repr_mimebundle_() else: return x @@ -565,13 +567,6 @@ 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 @@ -583,8 +578,8 @@ def close(self): Closes the underlying comm. When the comm is closed, all of the widget views are automatically removed from the front-end.""" - self.comm = None self._repr_mimebundle_ = None + self.comm = None def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end, if it exists. @@ -722,7 +717,7 @@ def notify_change(self, change): def __repr__(self): rep = self._gen_repr_from_keys(self._repr_keys()) - return 'closed: ' + rep if self.closed else rep + return 'closed: ' + rep if not self._repr_mimebundle_ else rep #------------------------------------------------------------------------- # Support methods diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index bb5570145f..92e216d824 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -14,7 +14,6 @@ from .widget_core import CoreWidget from .docutils import doc_subst from traitlets import Unicode, CaselessStrEnum, TraitType, observe -import sys _doc_snippets = {} _doc_snippets['box_params'] = """ @@ -27,20 +26,12 @@ which applies no pre-defined style. """ -# TODO: remove once 3.8 support is dropped. -if sys.version_info < (3, 9): - class Children(TraitType): - default_value = () +class Children(TraitType): + default_value = () - def validate(self, obj:'Box', value): - return tuple(v for v in value if isinstance(v, Widget) and not v.closed) -else: - class Children(TraitType[tuple[Widget],tuple[Widget]]): - default_value = () + def validate(self, obj, value): + return tuple(v for v in value if getattr(v, '_repr_mimebundle_', None)) - 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) @@ -66,13 +57,13 @@ 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 = Children(help="List of widget children").tag( + children:"tuple[Widget]" = 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): if children: kwargs['children'] = children @@ -86,22 +77,30 @@ def _box_observe_children(self, change): ref = weakref.ref(self) def handler(change): self_ = ref() - if self_ and change['owner']: - # Re-validation will discard all closed widgets. + if self_ and change['owner'] in self_.children: + # Re-validate children. + # tip: Use the context `hold_trait_notifications` + # to close multiple children at once. 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') + try: + w.observe(handler, names='comm') + except Exception: + pass if change['old']: for w in set(change['old']).difference(change['new']): try: w.unobserve(handler, names='comm') - except ValueError: - pass - + except Exception: + pass + + def close(self): + self.children = () + super().close() @register @doc_subst(_doc_snippets) @@ -164,3 +163,4 @@ class GridBox(Box): """ _model_name = Unicode('GridBoxModel').tag(sync=True) _view_name = Unicode('GridBoxView').tag(sync=True) + _box_observe_children = None \ No newline at end of file From 66ea63998aa96e3c63e99463ed6735d9eef6f6da Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Wed, 13 Mar 2024 07:31:39 +1100 Subject: [PATCH 12/38] Made observing box children an opt-in feature with the keyword only argument `observe_children`. Updated TestBox. --- .../widgets/tests/test_widget_box.py | 138 +++++++++--------- .../ipywidgets/widgets/widget_box.py | 23 ++- 2 files changed, 87 insertions(+), 74 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py index 66f85bf183..2ad1ea6d1d 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py @@ -36,70 +36,74 @@ def test_construction_invalid_style(self): widgets.Box(box_style='invalid') -def test_box_gc(): - # Test Box gc collected and children lifecycle managed. - deleted = False - b = widgets.VBox(children=[widgets.Button(description='button')]) - - def on_delete(): - nonlocal deleted - deleted = True - - weakref.finalize(b, on_delete) - del b - gc.collect() - assert deleted - -def test_box_child_closed_observe_childen(): - - b1 = widgets.Button(description='button') - b = widgets.VBox(children=[b1], observe_children=True) - b1.close() - assert b1 not in b.children - -def test_box_child_closed_not_observe_childen(): - - b1 = widgets.Button(description='button') - b = widgets.GridBox(children=[b1], observe_children=False) - b1.close() - assert b1 in b.children - -def test_box_gc_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 = widgets.VBox( - children=[ - widgets.Button(description="b0"), - widgets.Button(description="b1"), - widgets.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 widgets.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." - - # 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 b1.comm, 'A removed widget should remain alive.' - - # b2 shouldn't have any strong references so should be deleted. - assert ids[2] not in widgets.widget._instances, 'b2 should have been auto deleted.' + def test_gc(test): + # Test Box gc collected and children lifecycle managed. + deleted = False + b = widgets.VBox(children=[widgets.Button(description='button')]) + + def on_delete(): + nonlocal deleted + deleted = True + + weakref.finalize(b, on_delete) + del b + gc.collect() + assert deleted + + def test_child_closed_not_observe_childen(self): + + b1 = widgets.Button(description='button') + b = widgets.Box([b1], observe_children=False) + b1.close() + assert b1 in b.children + + def test_child_closed_observe_childen(self): + + b1 = widgets.Button(description='button') + b = widgets.Box([b1], observe_children=True) + b1.close() + assert b1 not in b.children + model_id = b.model_id + del b + assert model_id not in widgets.widget._instances + + + def test_box_gc_advanced(self): + # 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 = widgets.VBox( + children=[ + widgets.Button(description="b0"), + widgets.Button(description="b1"), + widgets.Button(description="b2"), + ], observe_children=True + ) + + 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 widgets.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." + + # 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 b1.comm, 'A removed widget should remain alive.' + + # b2 shouldn't have any strong references so should be deleted. + assert ids[2] not in widgets.widget._instances, 'b2 should have been auto deleted.' diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index 92e216d824..e222b9a280 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -20,6 +20,10 @@ children: iterable of Widget instances list of widgets to display + observe_children: bool + When enabled, the child comm will be observed. When any widget in children is + the children will be updated discarding closed widgets. + box_style: str one of 'success', 'info', 'warning' or 'danger', or ''. Applies a predefined style to the box. Defaults to '', @@ -53,6 +57,7 @@ class Box(DOMWidget, CoreWidget): """ _model_name = Unicode('BoxModel').tag(sync=True) _view_name = Unicode('BoxView').tag(sync=True) + _children_handlers = weakref.WeakKeyDictionary() # Child widgets in the container. # Using a tuple here to force reassignment to update the list. @@ -64,15 +69,19 @@ class Box(DOMWidget, CoreWidget): values=['success', 'info', 'warning', 'danger', ''], default_value='', help="""Use a predefined styling for the box.""").tag(sync=True) - def __init__(self, children=(), **kwargs): + def __init__(self, children=(), *, observe_children=False, **kwargs): + if observe_children: + self.observe(self._box_observe_children, names='children') if children: kwargs['children'] = children super().__init__(**kwargs) - @observe('children') - def _box_observe_children(self, change): + + @staticmethod + def _box_observe_children(change): + self:Box = change['owner'] # Monitor widgets for when the comm is closed. - handler = getattr(self, "_widget_children_comm_handler", None) + handler = self._children_handlers.get(self) if not handler: ref = weakref.ref(self) def handler(change): @@ -83,7 +92,7 @@ def handler(change): # to close multiple children at once. self_.children = self_.children - self._widget_children_comm_handler = handler + self._children_handlers[self] = handler if change['new']: w:Widget for w in set(change['new']).difference(change['old'] or ()): @@ -100,6 +109,7 @@ def handler(change): def close(self): self.children = () + self._children_handlers.pop(self, None) super().close() @register @@ -162,5 +172,4 @@ class GridBox(Box): >>> widgets.GridBox([title_widget, slider, button1, button2], layout=layout) """ _model_name = Unicode('GridBoxModel').tag(sync=True) - _view_name = Unicode('GridBoxView').tag(sync=True) - _box_observe_children = None \ No newline at end of file + _view_name = Unicode('GridBoxView').tag(sync=True) \ No newline at end of file From 6ff5b23e325f4490dd6fae4fe2b8f3b2eb694c31 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Thu, 14 Mar 2024 18:42:03 +1100 Subject: [PATCH 13/38] Update lock file --- yarn.lock | 70 +++++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9f0c63063f..66b13a6d58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -692,11 +692,11 @@ __metadata: languageName: node linkType: hard -"@jupyter-widgets/base-manager@^1.0.7, @jupyter-widgets/base-manager@workspace:packages/base-manager": +"@jupyter-widgets/base-manager@^1.0.8, @jupyter-widgets/base-manager@workspace:packages/base-manager": version: 0.0.0-use.local resolution: "@jupyter-widgets/base-manager@workspace:packages/base-manager" dependencies: - "@jupyter-widgets/base": ^6.0.6 + "@jupyter-widgets/base": ^6.0.7 "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@lumino/coreutils": ^1.11.1 || ^2 "@types/base64-js": ^1.2.5 @@ -731,7 +731,7 @@ __metadata: languageName: unknown linkType: soft -"@jupyter-widgets/base@^6.0.6, @jupyter-widgets/base@workspace:packages/base": +"@jupyter-widgets/base@^6.0.7, @jupyter-widgets/base@workspace:packages/base": version: 0.0.0-use.local resolution: "@jupyter-widgets/base@workspace:packages/base" dependencies: @@ -774,11 +774,11 @@ __metadata: languageName: unknown linkType: soft -"@jupyter-widgets/controls@^5.0.7, @jupyter-widgets/controls@workspace:packages/controls": +"@jupyter-widgets/controls@^5.0.8, @jupyter-widgets/controls@workspace:packages/controls": version: 0.0.0-use.local resolution: "@jupyter-widgets/controls@workspace:packages/controls" dependencies: - "@jupyter-widgets/base": ^6.0.6 + "@jupyter-widgets/base": ^6.0.7 "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@lumino/algorithm": ^1.9.1 || ^2.1 "@lumino/domutils": ^1.8.1 || ^2.1 @@ -829,9 +829,9 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/example-web1@workspace:examples/web1" dependencies: - "@jupyter-widgets/base": ^6.0.6 - "@jupyter-widgets/base-manager": ^1.0.7 - "@jupyter-widgets/controls": ^5.0.7 + "@jupyter-widgets/base": ^6.0.7 + "@jupyter-widgets/base-manager": ^1.0.8 + "@jupyter-widgets/controls": ^5.0.8 chai: ^4.0.0 css-loader: ^6.5.1 karma: ^6.3.3 @@ -850,9 +850,9 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/example-web2@workspace:examples/web2" dependencies: - "@jupyter-widgets/base": ^6.0.6 - "@jupyter-widgets/base-manager": ^1.0.7 - "@jupyter-widgets/controls": ^5.0.7 + "@jupyter-widgets/base": ^6.0.7 + "@jupyter-widgets/base-manager": ^1.0.8 + "@jupyter-widgets/controls": ^5.0.8 codemirror: ^5.48.0 css-loader: ^6.5.1 font-awesome: ^4.7.0 @@ -865,9 +865,9 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/example-web3@workspace:examples/web3" dependencies: - "@jupyter-widgets/base": ^6.0.6 - "@jupyter-widgets/controls": ^5.0.7 - "@jupyter-widgets/html-manager": ^1.0.9 + "@jupyter-widgets/base": ^6.0.7 + "@jupyter-widgets/controls": ^5.0.8 + "@jupyter-widgets/html-manager": ^1.0.10 "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@types/codemirror": ^5.60.0 "@types/node": ^17.0.2 @@ -887,7 +887,7 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/example-web4@workspace:examples/web4" dependencies: - "@jupyter-widgets/html-manager": ^1.0.9 + "@jupyter-widgets/html-manager": ^1.0.10 css-loader: ^6.5.1 font-awesome: ^4.7.0 style-loader: ^3.3.1 @@ -895,16 +895,16 @@ __metadata: languageName: unknown linkType: soft -"@jupyter-widgets/html-manager@^1.0.9, @jupyter-widgets/html-manager@workspace:packages/html-manager": +"@jupyter-widgets/html-manager@^1.0.10, @jupyter-widgets/html-manager@workspace:packages/html-manager": version: 0.0.0-use.local resolution: "@jupyter-widgets/html-manager@workspace:packages/html-manager" dependencies: "@fortawesome/fontawesome-free": ^5.12.0 - "@jupyter-widgets/base": ^6.0.6 - "@jupyter-widgets/base-manager": ^1.0.7 - "@jupyter-widgets/controls": ^5.0.7 - "@jupyter-widgets/output": ^6.0.6 - "@jupyter-widgets/schema": ^0.5.3 + "@jupyter-widgets/base": ^6.0.7 + "@jupyter-widgets/base-manager": ^1.0.8 + "@jupyter-widgets/controls": ^5.0.8 + "@jupyter-widgets/output": ^6.0.7 + "@jupyter-widgets/schema": ^0.5.4 "@jupyterlab/outputarea": ^3.0.0 || ^4.0.0 "@jupyterlab/rendermime": ^3.0.0 || ^4.0.0 "@jupyterlab/rendermime-interfaces": ^3.0.0 || ^4.0.0 @@ -941,10 +941,10 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/jupyterlab-manager@workspace:python/jupyterlab_widgets" dependencies: - "@jupyter-widgets/base": ^6.0.6 - "@jupyter-widgets/base-manager": ^1.0.7 - "@jupyter-widgets/controls": ^5.0.7 - "@jupyter-widgets/output": ^6.0.6 + "@jupyter-widgets/base": ^6.0.7 + "@jupyter-widgets/base-manager": ^1.0.8 + "@jupyter-widgets/controls": ^5.0.8 + "@jupyter-widgets/output": ^6.0.7 "@jupyterlab/application": ^3.0.0 || ^4.0.0 "@jupyterlab/builder": ^3.0.0 || ^4.0.0 "@jupyterlab/cells": ^3.0.0 || ^4.0.0 @@ -987,11 +987,11 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/notebook-manager@workspace:python/widgetsnbextension" dependencies: - "@jupyter-widgets/base": ^6.0.6 - "@jupyter-widgets/base-manager": ^1.0.7 - "@jupyter-widgets/controls": ^5.0.7 - "@jupyter-widgets/html-manager": ^1.0.9 - "@jupyter-widgets/output": ^6.0.6 + "@jupyter-widgets/base": ^6.0.7 + "@jupyter-widgets/base-manager": ^1.0.8 + "@jupyter-widgets/controls": ^5.0.8 + "@jupyter-widgets/html-manager": ^1.0.10 + "@jupyter-widgets/output": ^6.0.7 "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@lumino/messaging": ^1.10.1 || ^2.1 "@lumino/widgets": ^1.30.0 || ^2.1 @@ -1005,17 +1005,17 @@ __metadata: languageName: unknown linkType: soft -"@jupyter-widgets/output@^6.0.6, @jupyter-widgets/output@workspace:packages/output": +"@jupyter-widgets/output@^6.0.7, @jupyter-widgets/output@workspace:packages/output": version: 0.0.0-use.local resolution: "@jupyter-widgets/output@workspace:packages/output" dependencies: - "@jupyter-widgets/base": ^6.0.6 + "@jupyter-widgets/base": ^6.0.7 rimraf: ^3.0.2 typescript: ~4.9.4 languageName: unknown linkType: soft -"@jupyter-widgets/schema@^0.5.3, @jupyter-widgets/schema@workspace:packages/schema": +"@jupyter-widgets/schema@^0.5.4, @jupyter-widgets/schema@workspace:packages/schema": version: 0.0.0-use.local resolution: "@jupyter-widgets/schema@workspace:packages/schema" languageName: unknown @@ -14908,11 +14908,11 @@ __metadata: "typescript@patch:typescript@npm%3A~4.9.4#~builtin": version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587" + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 1f8f3b6aaea19f0f67cba79057674ba580438a7db55057eb89cc06950483c5d632115c14077f6663ea76fd09fce3c190e6414bb98582ec80aa5a4eaf345d5b68 + checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d languageName: node linkType: hard From 4c9004d844b2bd7cc935393c2b95ec9ca0a14aa2 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Thu, 14 Mar 2024 18:44:24 +1100 Subject: [PATCH 14/38] Ran jlpm lint --- docs/source/dev_release.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/dev_release.md b/docs/source/dev_release.md index 054c486299..7a9be90ee6 100644 --- a/docs/source/dev_release.md +++ b/docs/source/dev_release.md @@ -58,11 +58,14 @@ Lerna will prompt you for version numbers for each of the changed npm packages i ## Configure twine username If you have 2FA on, make sure you have your `~/.pypirc` file set to: + ``` [pypi] username = __token__ ``` + Or set the environment variable + ``` export TWINE_USERNAME=__token__ ``` From d182a479e14330247fbbc3730a7ad384d7ca45e9 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Fri, 15 Mar 2024 08:07:38 +1100 Subject: [PATCH 15/38] _widget_to_json - change evaluation order to check for a widget first (most common object). - raise NotImplementedError for objects with _repr_mimebundle_ that aren't widgets. --- .../widgets/tests/test_widget_box.py | 20 ++++++++++++- .../ipywidgets/ipywidgets/widgets/widget.py | 29 +++++++++++++------ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py index 2ad1ea6d1d..bb6f29e294 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py @@ -8,6 +8,8 @@ from traitlets import TraitError +import pytest + import ipywidgets as widgets @@ -68,7 +70,7 @@ def test_child_closed_observe_childen(self): assert model_id not in widgets.widget._instances - def test_box_gc_advanced(self): + def test_gc_advanced(self): # 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. @@ -107,3 +109,19 @@ def on_delete(): # b2 shouldn't have any strong references so should be deleted. assert ids[2] not in widgets.widget._instances, 'b2 should have been auto deleted.' + + + def test_repr_mimebundle(self): + + b1 = widgets.Button() + + class wrapper: + _repr_mimebundle_ = b1._repr_mimebundle_ + + w = wrapper() + with pytest.raises(NotImplementedError): + b = widgets.Box([b1, w]) + + + + \ No newline at end of file diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index b792642522..882756c720 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -46,17 +46,23 @@ def envset(name, default): _instances : typing.MutableMapping[str, "Widget"] = weakref.WeakValueDictionary() def _widget_to_json(x, obj): - if isinstance(x, dict): - return {k: _widget_to_json(v, obj) for k, v in x.items()} - elif isinstance(x, (list, tuple)): - return [_widget_to_json(v, obj) for v in x] - elif isinstance(x, Widget): + if isinstance(x, Widget): if not x._repr_mimebundle_: + # a closed widget will not be found at the frontend so raise an error here. msg = f"Widget is {x!r}" raise RuntimeError(msg) - return "IPY_MODEL_" + x.model_id - elif hasattr(x, '_repr_mimebundle_'): - return x._repr_mimebundle_() + # _model_id provides faster access if a comm is already open which typically it is. + return "IPY_MODEL_" + x._model_id or x.model_id + elif isinstance(x, (list, tuple)): + return [_widget_to_json(v, obj) for v in x] + elif isinstance(x, dict): + return {k: _widget_to_json(v, obj) for k, v in x.items()} + elif hasattr(x, "_repr_mimebundle_"): + msg = ( + "Support for _repr_mimebundle_ not yet implemented in the Box widget." + f"The object may be viewed with Output widget instead. Invalid object: {x!r}" + ) + raise NotImplementedError(msg) else: return x @@ -297,7 +303,8 @@ class Widget(LoggingHasTraits): #------------------------------------------------------------------------- _widget_construction_callback = None _control_comm = None - + _model_id = '' + @_staticproperty def widgets(): # Because this is a static attribute, it will be accessed when initializing this class. In that case, since a user @@ -545,6 +552,7 @@ def _comm_changed(self, change): change['old'].close() _instances.pop(change['old'].comm_id, None) if change['new']: + self._model_id = change['new'].comm_id _instances[change['new'].comm_id] = self # prevent memory leaks by using a weak reference to self. @@ -558,6 +566,9 @@ def _handle_msg(msg): self_._show_traceback(self_._handle_msg, e) change['new'].on_msg(_handle_msg) + else: + self._model_id = '' + @property From 5448eccddb5cdb17b172397e40fd119abd4abafe Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 16 Mar 2024 08:31:03 +1100 Subject: [PATCH 16/38] Improve error message in `_widget_to_json`. --- python/ipywidgets/ipywidgets/widgets/widget.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 882756c720..337ec21cbf 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -51,7 +51,7 @@ def _widget_to_json(x, obj): # a closed widget will not be found at the frontend so raise an error here. msg = f"Widget is {x!r}" raise RuntimeError(msg) - # _model_id provides faster access if a comm is already open which typically it is. + # _model_id provides faster access if its comm is already open. return "IPY_MODEL_" + x._model_id or x.model_id elif isinstance(x, (list, tuple)): return [_widget_to_json(v, obj) for v in x] @@ -59,8 +59,9 @@ def _widget_to_json(x, obj): return {k: _widget_to_json(v, obj) for k, v in x.items()} elif hasattr(x, "_repr_mimebundle_"): msg = ( - "Support for _repr_mimebundle_ not yet implemented in the Box widget." - f"The object may be viewed with Output widget instead. Invalid object: {x!r}" + f"{x!r} is not a widget, but provides a `_repr_mimebundle_` attribute. " + "Support for direct `_repr_mimebundle_` has not been implemented yet. In the " + "meantime, it should be possible to view the object wih an `Output` widget." ) raise NotImplementedError(msg) else: From d1379cb6320a97e2adfee711d832deb346e267cd Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 18 Mar 2024 08:59:02 +1100 Subject: [PATCH 17/38] Update method Widget.get_view_spec. --- python/ipywidgets/ipywidgets/widgets/tests/test_widget.py | 7 ++++++- python/ipywidgets/ipywidgets/widgets/widget.py | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index 6b5cd6854d..3fd20ae1cb 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -99,10 +99,15 @@ 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): + with pytest.raises(RuntimeError, match='This widget is closed'): button.open() + with pytest.raises(RuntimeError, match='This widget is closed'): + button.get_view_spec() def test_gc(): diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 337ec21cbf..a4af4e82a1 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -462,7 +462,10 @@ def _get_embed_state(self, drop_defaults=False): return state def get_view_spec(self): - return dict(version_major=2, version_minor=0, model_id=self.model_id) + if not self._repr_mimebundle_: + msg = f"This widget is {self!r}" + raise RuntimeError(msg) + return {"version_major":2, "version_minor":0, "model_id":self._model_id or self.model_id} #------------------------------------------------------------------------- # Traits From cecbb88cfe18e18ad37ae4a5150728c2f4be06b8 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Wed, 20 Mar 2024 21:10:53 +1100 Subject: [PATCH 18/38] Optimised Widget._send --- python/ipywidgets/ipywidgets/widgets/widget.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index a4af4e82a1..3d31087343 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -859,8 +859,9 @@ def _repr_mimebundle_(self, **kwargs): def _send(self, msg, buffers=None): """Sends a message to the model in the front-end.""" - if self.comm is not None and (self.comm.kernel is not None if hasattr(self.comm, "kernel") else True): - self.comm.send(data=msg, buffers=buffers) + comm = self.comm + if comm is not None and getattr(comm, "kernel", True): + comm.send(data=msg, buffers=buffers) def _repr_keys(self): traits = self.traits() From 5ad372309934dbc3dfbbb71fb41b9ca01d9187a7 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Tue, 2 Apr 2024 12:57:48 +1100 Subject: [PATCH 19/38] Restore type hint info for Children. --- .../ipywidgets/widgets/widget_box.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index e222b9a280..166f68ccb5 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -29,12 +29,20 @@ Applies a predefined style to the box. Defaults to '', which applies no pre-defined style. """ - -class Children(TraitType): - default_value = () - - def validate(self, obj, value): - return tuple(v for v in value if getattr(v, '_repr_mimebundle_', None)) +import sys +if sys.version_info < (3, 11): + class Children(TraitType): + default_value = () + + def validate(self, obj, value): + return tuple(v for v in value if getattr(v, '_repr_mimebundle_', None)) +else: + import typing + class Children(TraitType[tuple[Widget,...], typing.Iterable[Widget]]): + default_value = () + + def validate(self, obj, value): + return tuple(v for v in value if getattr(v, '_repr_mimebundle_', None)) @register From 2e8c78454f4c7f0ef13d73dc6ecbefb921078a9f Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 6 Apr 2024 18:42:33 +1100 Subject: [PATCH 20/38] Remove redundant type hint from `Box.children`. --- python/ipywidgets/ipywidgets/widgets/widget_box.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index 166f68ccb5..fcbade781f 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -70,7 +70,7 @@ 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:"tuple[Widget]" = Children(help="List of widget children").tag( + children = Children(help="List of widget children").tag( sync=True, **widget_serialization) box_style = CaselessStrEnum( From 21cb6fa2d697529f86a28d0b215e2d244b484b52 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 7 Apr 2024 10:56:44 +1000 Subject: [PATCH 21/38] Fix Checkbox tooltips Fix 'Exception' for a Box that has been provided a tooltip. --- packages/controls/src/widget_bool.ts | 12 ++++-------- packages/controls/src/widget_box.ts | 3 +++ python/ipywidgets/ipywidgets/widgets/widget_box.py | 3 +++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/controls/src/widget_bool.ts b/packages/controls/src/widget_bool.ts index a08d9ff693..62ff0df2c3 100644 --- a/packages/controls/src/widget_bool.ts +++ b/packages/controls/src/widget_bool.ts @@ -153,8 +153,6 @@ export class CheckboxView extends DescriptionView { this.descriptionSpan.textContent = description; } this.typeset(this.descriptionSpan); - this.descriptionSpan.title = description; - this.checkbox.title = description; } /** @@ -181,13 +179,11 @@ export class CheckboxView extends DescriptionView { } updateTooltip(): void { + super.updateTooltip(); if (!this.checkbox) return; // we might be constructing the parent - const title = this.model.get('tooltip'); - if (!title) { - this.checkbox.removeAttribute('title'); - } else if (this.model.get('description').length === 0) { - this.checkbox.setAttribute('title', title); - } + const title = this.model.get('tooltip') || ''; + this.checkbox.setAttribute('title', title); + this.descriptionSpan.setAttribute('title', title); } events(): { [e: string]: string } { diff --git a/packages/controls/src/widget_box.ts b/packages/controls/src/widget_box.ts index 2559d50d02..889dc89092 100644 --- a/packages/controls/src/widget_box.ts +++ b/packages/controls/src/widget_box.ts @@ -91,6 +91,9 @@ export class BoxView extends DOMWidgetView { this.set_box_style(); } + updateTooltip(): void { + return; + } update_children(): void { this.children_views ?.update(this.model.get('children')) diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index fcbade781f..4a882b7ec9 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -67,6 +67,9 @@ class Box(DOMWidget, CoreWidget): _view_name = Unicode('BoxView').tag(sync=True) _children_handlers = weakref.WeakKeyDictionary() + # Tooltip is not allowed for containers (override for DOMWidget). + tooltip = None + # 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. From 24af66b38c67647a32a0b954688b6d7259684831 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 7 Apr 2024 11:16:15 +1000 Subject: [PATCH 22/38] Fix tooltips for TextareaView, TextView, SelectionView. --- packages/controls/src/widget_description.ts | 2 +- packages/controls/src/widget_selection.ts | 9 +++------ packages/controls/src/widget_string.ts | 18 ++++++------------ 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/controls/src/widget_description.ts b/packages/controls/src/widget_description.ts index 2b0ccdcf7f..b1cc71a0a8 100644 --- a/packages/controls/src/widget_description.ts +++ b/packages/controls/src/widget_description.ts @@ -99,7 +99,7 @@ export class DescriptionView extends DOMWidgetView { updateTooltip(): void { if (!this.label) return; - this.label.title = this.model.get('tooltip'); + this.label.title = this.model.get('tooltip') || ''; } label: HTMLLabelElement; diff --git a/packages/controls/src/widget_selection.ts b/packages/controls/src/widget_selection.ts index 11174799cd..3b26881700 100644 --- a/packages/controls/src/widget_selection.ts +++ b/packages/controls/src/widget_selection.ts @@ -68,13 +68,10 @@ export class SelectionView extends DescriptionView { } updateTooltip(): void { + super.updateTooltip(); if (!this.listbox) return; // we might be constructing the parent - const title = this.model.get('tooltip'); - if (!title) { - this.listbox.removeAttribute('title'); - } else if (this.model.get('description').length === 0) { - this.listbox.setAttribute('title', title); - } + const title = this.model.get('tooltip') || ''; + this.listbox.setAttribute('title', title); } listbox: HTMLSelectElement; diff --git a/packages/controls/src/widget_string.ts b/packages/controls/src/widget_string.ts index 4bdef6c28d..d2cd6dcc18 100644 --- a/packages/controls/src/widget_string.ts +++ b/packages/controls/src/widget_string.ts @@ -379,13 +379,10 @@ export class TextareaView extends StringView { } updateTooltip(): void { + super.updateTooltip(); if (!this.textbox) return; // we might be constructing the parent - const title = this.model.get('tooltip'); - if (!title) { - this.textbox.removeAttribute('title'); - } else if (this.model.get('description').length === 0) { - this.textbox.setAttribute('title', title); - } + const title = this.model.get('tooltip') || ''; + this.textbox.setAttribute('title', title); } events(): { [e: string]: string } { @@ -505,13 +502,10 @@ export class TextView extends StringView { } updateTooltip(): void { + super.updateTooltip(); if (!this.textbox) return; // we might be constructing the parent - const title = this.model.get('tooltip'); - if (!title) { - this.textbox.removeAttribute('title'); - } else if (this.model.get('description').length === 0) { - this.textbox.setAttribute('title', title); - } + const title = this.model.get('tooltip') || ''; + this.textbox.setAttribute('title', title); } update(options?: any): void { From ce31091ca61fb2a7ee8015d3e8a32411d8a2c29d Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Fri, 3 May 2024 18:43:49 +1000 Subject: [PATCH 23/38] Fix invalid package dependencies @lumino/messaging & @lumino/algorithm. Remove observe_children from 'Box' --- packages/base-manager/package.json | 4 +- packages/base/package.json | 2 +- packages/controls/package.json | 4 +- packages/html-manager/package.json | 4 +- .../widgets/tests/test_widget_box.py | 75 ----------------- .../ipywidgets/widgets/widget_box.py | 66 +++------------ python/jupyterlab_widgets/package.json | 2 +- python/widgetsnbextension/package.json | 2 +- yarn.lock | 80 +++++++++---------- 9 files changed, 62 insertions(+), 177 deletions(-) diff --git a/packages/base-manager/package.json b/packages/base-manager/package.json index 70fff0f0b6..42d5ddd2f1 100644 --- a/packages/base-manager/package.json +++ b/packages/base-manager/package.json @@ -36,7 +36,7 @@ "@jupyterlab/services": "^6.0.0 || ^7.0.0", "@lumino/coreutils": "^1.11.1 || ^2", "base64-js": "^1.2.1", - "sanitize-html": "^2.3" + "sanitize-html": "^2.13.0" }, "devDependencies": { "@types/base64-js": "^1.2.5", @@ -44,7 +44,7 @@ "@types/chai-as-promised": "^7.1.0", "@types/expect.js": "^0.3.29", "@types/mocha": "^9.0.0", - "@types/sanitize-html": "^2.6.0", + "@types/sanitize-html": "^2.11.0", "@types/sinon": "^10.0.2", "@types/sinon-chai": "^3.2.2", "chai": "^4.0.0", diff --git a/packages/base/package.json b/packages/base/package.json index a7cf086a94..f21761f6ff 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -35,7 +35,7 @@ "dependencies": { "@jupyterlab/services": "^6.0.0 || ^7.0.0", "@lumino/coreutils": "^1.11.1 || ^2.1", - "@lumino/messaging": "^1.10.1 || ^2.1", + "@lumino/messaging": "^1.10.1 || ^2.0.1", "@lumino/widgets": "^1.30.0 || ^2.1", "@types/backbone": "1.4.14", "@types/lodash": "^4.14.134", diff --git a/packages/controls/package.json b/packages/controls/package.json index 6af6b76b00..0e044940bd 100644 --- a/packages/controls/package.json +++ b/packages/controls/package.json @@ -35,9 +35,9 @@ }, "dependencies": { "@jupyter-widgets/base": "^6.0.7", - "@lumino/algorithm": "^1.9.1 || ^2.1", + "@lumino/algorithm": "^1.9.2 || ^2.0.1", "@lumino/domutils": "^1.8.1 || ^2.1", - "@lumino/messaging": "^1.10.1 || ^2.1", + "@lumino/messaging": "^1.10.1 || ^2.0.1", "@lumino/signaling": "^1.10.1 || ^2.1", "@lumino/widgets": "^1.30.0 || ^2.1", "d3-color": "^3.0.1", diff --git a/packages/html-manager/package.json b/packages/html-manager/package.json index f1930d10fd..de66cd2faa 100644 --- a/packages/html-manager/package.json +++ b/packages/html-manager/package.json @@ -44,7 +44,7 @@ "@jupyterlab/outputarea": "^3.0.0 || ^4.0.0", "@jupyterlab/rendermime": "^3.0.0 || ^4.0.0", "@jupyterlab/rendermime-interfaces": "^3.0.0 || ^4.0.0", - "@lumino/messaging": "^1.10.1 || ^2.1", + "@lumino/messaging": "^1.10.1 || ^2.0.1", "@lumino/widgets": "^1.30.0 || ^2.1", "ajv": "^8.6.0", "jquery": "^3.1.1" @@ -53,7 +53,7 @@ "@types/jquery": "^3.5.16", "@types/mocha": "^9.0.0", "@types/node": "^17.0.2", - "@types/sanitize-html": "^2.6.0", + "@types/sanitize-html": "^2.11.0", "chai": "^4.0.0", "css-loader": "^6.5.1", "karma": "^6.3.3", diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py index bb6f29e294..ce891c5c07 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py @@ -3,13 +3,10 @@ import gc import weakref - from unittest import TestCase from traitlets import TraitError -import pytest - import ipywidgets as widgets @@ -52,76 +49,4 @@ def on_delete(): gc.collect() assert deleted - def test_child_closed_not_observe_childen(self): - - b1 = widgets.Button(description='button') - b = widgets.Box([b1], observe_children=False) - b1.close() - assert b1 in b.children - - def test_child_closed_observe_childen(self): - - b1 = widgets.Button(description='button') - b = widgets.Box([b1], observe_children=True) - b1.close() - assert b1 not in b.children - model_id = b.model_id - del b - assert model_id not in widgets.widget._instances - - - def test_gc_advanced(self): - # 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 = widgets.VBox( - children=[ - widgets.Button(description="b0"), - widgets.Button(description="b1"), - widgets.Button(description="b2"), - ], observe_children=True - ) - - 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 widgets.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." - - # 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 b1.comm, 'A removed widget should remain alive.' - - # b2 shouldn't have any strong references so should be deleted. - assert ids[2] not in widgets.widget._instances, 'b2 should have been auto deleted.' - - - def test_repr_mimebundle(self): - - b1 = widgets.Button() - - class wrapper: - _repr_mimebundle_ = b1._repr_mimebundle_ - - w = wrapper() - with pytest.raises(NotImplementedError): - b = widgets.Box([b1, w]) - - - \ No newline at end of file diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index 4a882b7ec9..3e01a1a61f 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -1,3 +1,4 @@ + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. @@ -7,42 +8,33 @@ group other widgets together and control their relative layouts. """ +from __future__ import annotations + +import typing import weakref -from .widget import register, widget_serialization, Widget +from traitlets import CaselessStrEnum, TraitType, Unicode + +from .docutils import doc_subst from .domwidget import DOMWidget +from .widget import Widget, register, widget_serialization from .widget_core import CoreWidget -from .docutils import doc_subst -from traitlets import Unicode, CaselessStrEnum, TraitType, observe _doc_snippets = {} _doc_snippets['box_params'] = """ children: iterable of Widget instances list of widgets to display - observe_children: bool - When enabled, the child comm will be observed. When any widget in children is - the children will be updated discarding closed widgets. - box_style: str one of 'success', 'info', 'warning' or 'danger', or ''. Applies a predefined style to the box. Defaults to '', which applies no pre-defined style. """ -import sys -if sys.version_info < (3, 11): - class Children(TraitType): - default_value = () +class Children(TraitType["tuple[Widget,...]", typing.Iterable[Widget]]): + default_value = () - def validate(self, obj, value): - return tuple(v for v in value if getattr(v, '_repr_mimebundle_', None)) -else: - import typing - class Children(TraitType[tuple[Widget,...], typing.Iterable[Widget]]): - default_value = () - - def validate(self, obj, value): - return tuple(v for v in value if getattr(v, '_repr_mimebundle_', None)) + def validate(self, obj, value): + return tuple(v for v in value if isinstance(v, Widget) and v.comm) @register @@ -80,43 +72,11 @@ class Box(DOMWidget, CoreWidget): values=['success', 'info', 'warning', 'danger', ''], default_value='', help="""Use a predefined styling for the box.""").tag(sync=True) - def __init__(self, children=(), *, observe_children=False, **kwargs): - if observe_children: - self.observe(self._box_observe_children, names='children') + def __init__(self, children=(), **kwargs): if children: kwargs['children'] = children super().__init__(**kwargs) - - @staticmethod - def _box_observe_children(change): - self:Box = change['owner'] - # Monitor widgets for when the comm is closed. - handler = self._children_handlers.get(self) - if not handler: - ref = weakref.ref(self) - def handler(change): - self_ = ref() - if self_ and change['owner'] in self_.children: - # Re-validate children. - # tip: Use the context `hold_trait_notifications` - # to close multiple children at once. - self_.children = self_.children - - self._children_handlers[self] = handler - if change['new']: - w:Widget - for w in set(change['new']).difference(change['old'] or ()): - try: - w.observe(handler, names='comm') - except Exception: - pass - if change['old']: - for w in set(change['old']).difference(change['new']): - try: - w.unobserve(handler, names='comm') - except Exception: - pass def close(self): self.children = () diff --git a/python/jupyterlab_widgets/package.json b/python/jupyterlab_widgets/package.json index 35fe2db8c8..05ba093f55 100644 --- a/python/jupyterlab_widgets/package.json +++ b/python/jupyterlab_widgets/package.json @@ -62,7 +62,7 @@ "@jupyterlab/services": "^6.0.0 || ^7.0.0", "@jupyterlab/settingregistry": "^3.0.0 || ^4.0.0", "@jupyterlab/translation": "^3.0.0 || ^4.0.0", - "@lumino/algorithm": "^1.11.1 || ^2.0.0", + "@lumino/algorithm": "^1.9.2 || ^2.0.1", "@lumino/coreutils": "^1.11.1 || ^2.1", "@lumino/disposable": "^1.10.1 || ^2.1", "@lumino/properties": "^1.8.1 || ^2.1", diff --git a/python/widgetsnbextension/package.json b/python/widgetsnbextension/package.json index 5a51c9afa7..5a81a1ed02 100644 --- a/python/widgetsnbextension/package.json +++ b/python/widgetsnbextension/package.json @@ -28,7 +28,7 @@ "@jupyter-widgets/html-manager": "^1.0.10", "@jupyter-widgets/output": "^6.0.7", "@jupyterlab/services": "^6.0.0 || ^7.0.0", - "@lumino/messaging": "^1.10.1 || ^2.1", + "@lumino/messaging": "^1.10.1 || ^2.0.1", "@lumino/widgets": "^1.30.0 || ^2.1", "backbone": "1.4.0" }, diff --git a/yarn.lock b/yarn.lock index 66b13a6d58..09ab1a9f2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -704,7 +704,7 @@ __metadata: "@types/chai-as-promised": ^7.1.0 "@types/expect.js": ^0.3.29 "@types/mocha": ^9.0.0 - "@types/sanitize-html": ^2.6.0 + "@types/sanitize-html": ^2.11.0 "@types/sinon": ^10.0.2 "@types/sinon-chai": ^3.2.2 base64-js: ^1.2.1 @@ -723,7 +723,7 @@ __metadata: mocha: ^9.0.0 npm-run-all: ^4.1.5 rimraf: ^3.0.2 - sanitize-html: ^2.3 + sanitize-html: ^2.13.0 sinon: ^12.0.1 sinon-chai: ^3.3.0 typescript: ~4.9.4 @@ -737,7 +737,7 @@ __metadata: dependencies: "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@lumino/coreutils": ^1.11.1 || ^2.1 - "@lumino/messaging": ^1.10.1 || ^2.1 + "@lumino/messaging": ^1.10.1 || ^2.0.1 "@lumino/widgets": ^1.30.0 || ^2.1 "@types/backbone": 1.4.14 "@types/base64-js": ^1.2.5 @@ -780,9 +780,9 @@ __metadata: dependencies: "@jupyter-widgets/base": ^6.0.7 "@jupyterlab/services": ^6.0.0 || ^7.0.0 - "@lumino/algorithm": ^1.9.1 || ^2.1 + "@lumino/algorithm": ^1.9.2 || ^2.0.1 "@lumino/domutils": ^1.8.1 || ^2.1 - "@lumino/messaging": ^1.10.1 || ^2.1 + "@lumino/messaging": ^1.10.1 || ^2.0.1 "@lumino/signaling": ^1.10.1 || ^2.1 "@lumino/widgets": ^1.30.0 || ^2.1 "@types/d3-color": ^3.0.2 @@ -908,12 +908,12 @@ __metadata: "@jupyterlab/outputarea": ^3.0.0 || ^4.0.0 "@jupyterlab/rendermime": ^3.0.0 || ^4.0.0 "@jupyterlab/rendermime-interfaces": ^3.0.0 || ^4.0.0 - "@lumino/messaging": ^1.10.1 || ^2.1 + "@lumino/messaging": ^1.10.1 || ^2.0.1 "@lumino/widgets": ^1.30.0 || ^2.1 "@types/jquery": ^3.5.16 "@types/mocha": ^9.0.0 "@types/node": ^17.0.2 - "@types/sanitize-html": ^2.6.0 + "@types/sanitize-html": ^2.11.0 ajv: ^8.6.0 chai: ^4.0.0 css-loader: ^6.5.1 @@ -959,7 +959,7 @@ __metadata: "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@jupyterlab/settingregistry": ^3.0.0 || ^4.0.0 "@jupyterlab/translation": ^3.0.0 || ^4.0.0 - "@lumino/algorithm": ^1.11.1 || ^2.0.0 + "@lumino/algorithm": ^1.9.2 || ^2.0.1 "@lumino/coreutils": ^1.11.1 || ^2.1 "@lumino/disposable": ^1.10.1 || ^2.1 "@lumino/properties": ^1.8.1 || ^2.1 @@ -993,7 +993,7 @@ __metadata: "@jupyter-widgets/html-manager": ^1.0.10 "@jupyter-widgets/output": ^6.0.7 "@jupyterlab/services": ^6.0.0 || ^7.0.0 - "@lumino/messaging": ^1.10.1 || ^2.1 + "@lumino/messaging": ^1.10.1 || ^2.0.1 "@lumino/widgets": ^1.30.0 || ^2.1 backbone: 1.4.0 css-loader: ^6.5.1 @@ -2634,17 +2634,17 @@ __metadata: languageName: node linkType: hard -"@lumino/algorithm@npm:^1.11.1 || ^2.0.0, @lumino/algorithm@npm:^2.0.0": - version: 2.0.0 - resolution: "@lumino/algorithm@npm:2.0.0" - checksum: 663edf536e94397b449c6a2643a735e602fbb396dec86b56ad1193a768dce27c6e7da5ad0384aa90086ea44cbb64dde3f9d565e9fd81858f1eb0c6b4253f3b94 +"@lumino/algorithm@npm:^1.9.2 || ^2.0.1, @lumino/algorithm@npm:^2.0.1": + version: 2.0.1 + resolution: "@lumino/algorithm@npm:2.0.1" + checksum: cbf7fcf6ee6b785ea502cdfddc53d61f9d353dcb9659343511d5cd4b4030be2ff2ca4c08daec42f84417ab0318a3d9972a17319fa5231693e109ab112dcf8000 languageName: node linkType: hard -"@lumino/algorithm@npm:^1.9.1 || ^2.1, @lumino/algorithm@npm:^1.9.2": - version: 1.9.2 - resolution: "@lumino/algorithm@npm:1.9.2" - checksum: a89e7c63504236119634858e271db1cc649684d30ced5a6ebe2788af7c0837f1e05a6fd3047d8525eb756c42ce137f76b3688f75fd3ef915b71cd4f213dfbb96 +"@lumino/algorithm@npm:^2.0.0": + version: 2.0.0 + resolution: "@lumino/algorithm@npm:2.0.0" + checksum: 663edf536e94397b449c6a2643a735e602fbb396dec86b56ad1193a768dce27c6e7da5ad0384aa90086ea44cbb64dde3f9d565e9fd81858f1eb0c6b4253f3b94 languageName: node linkType: hard @@ -2659,15 +2659,6 @@ __metadata: languageName: node linkType: hard -"@lumino/collections@npm:^1.9.3": - version: 1.9.3 - resolution: "@lumino/collections@npm:1.9.3" - dependencies: - "@lumino/algorithm": ^1.9.2 - checksum: 1c87a12743eddd6f6b593e47945a5645e2f99ad61c5192499b0745e48ee9aff263c7145541e77dfeea4c9f50bdd017fddfa47bfc60e718de4f28533ce45bf8c3 - languageName: node - linkType: hard - "@lumino/collections@npm:^2.0.0": version: 2.0.0 resolution: "@lumino/collections@npm:2.0.0" @@ -2677,6 +2668,15 @@ __metadata: languageName: node linkType: hard +"@lumino/collections@npm:^2.0.1": + version: 2.0.1 + resolution: "@lumino/collections@npm:2.0.1" + dependencies: + "@lumino/algorithm": ^2.0.1 + checksum: 8a29b7973a388a33c5beda0819dcd2dc2aad51a8406dcfd4581b055a9f77a39dc5800f7a8b4ae3c0bb97ae7b56a7a869e2560ffb7a920a28e93b477ba05907d6 + languageName: node + linkType: hard + "@lumino/commands@npm:^2.1.1": version: 2.1.1 resolution: "@lumino/commands@npm:2.1.1" @@ -2739,13 +2739,13 @@ __metadata: languageName: node linkType: hard -"@lumino/messaging@npm:^1.10.1 || ^2.1": - version: 1.10.3 - resolution: "@lumino/messaging@npm:1.10.3" +"@lumino/messaging@npm:^1.10.1 || ^2.0.1": + version: 2.0.1 + resolution: "@lumino/messaging@npm:2.0.1" dependencies: - "@lumino/algorithm": ^1.9.2 - "@lumino/collections": ^1.9.3 - checksum: 1131e80379fa9b8a9b5d3418c90e25d4be48e2c92ec711518190772f9e8845a695bef45daddd06a129168cf6f158c8ad80ae86cb245f566e9195bbd9a0843b7a + "@lumino/algorithm": ^2.0.1 + "@lumino/collections": ^2.0.1 + checksum: 964c4651c374b17452b4252b7d71500b32d2ecd87c192fc5bcf5d3bd1070661d78d07edcac8eca7d1d6fd50aa25992505485e1296d6dd995691b8e349b652045 languageName: node linkType: hard @@ -3646,12 +3646,12 @@ __metadata: languageName: node linkType: hard -"@types/sanitize-html@npm:^2.6.0": - version: 2.9.0 - resolution: "@types/sanitize-html@npm:2.9.0" +"@types/sanitize-html@npm:^2.11.0": + version: 2.11.0 + resolution: "@types/sanitize-html@npm:2.11.0" dependencies: htmlparser2: ^8.0.0 - checksum: b60f42b740bbfb1b1434ce8b43925a38ecc608b60aa654fd009d2e22e33f324b61d370768c55bd2fd98e03de08518ffa8911d61606c483526fb931bb8b59d1b0 + checksum: a901d55d31cd946a7fce0130cc7cf6bcf56602af9c87291be77d8149c60e7afc47c83ca74c67c2d84e6ba029fe9bbd6f14f89a8cb30fbd185766eebc5722c251 languageName: node linkType: hard @@ -13501,9 +13501,9 @@ __metadata: languageName: node linkType: hard -"sanitize-html@npm:^2.3": - version: 2.10.0 - resolution: "sanitize-html@npm:2.10.0" +"sanitize-html@npm:^2.13.0": + version: 2.13.0 + resolution: "sanitize-html@npm:2.13.0" dependencies: deepmerge: ^4.2.2 escape-string-regexp: ^4.0.0 @@ -13511,7 +13511,7 @@ __metadata: is-plain-object: ^5.0.0 parse-srcset: ^1.0.2 postcss: ^8.3.11 - checksum: 0cb2bb330ed966a4d667b1890322dd868a67f527f87c04d7e3be1688fcfda20f7452a9a7744870751f51e255742e7264a287d9bcfcd64d4cd74a3c99f99c73d2 + checksum: d88602328306dbbddb9c5e2a5798783a3b38977a7ef40bf81dae31220d7fb583149c1046a33ec6817e9d96d172b1aaa9ea159776eb1ee08f6a0571150114c9bf languageName: node linkType: hard From 9f12504d4947422be2788229072bbb1af435e981 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Fri, 3 May 2024 18:57:21 +1000 Subject: [PATCH 24/38] Remove unnecessary weakrefs --- python/ipywidgets/ipywidgets/widgets/tests/test_widget.py | 2 +- python/ipywidgets/ipywidgets/widgets/widget_button.py | 6 ++---- python/ipywidgets/ipywidgets/widgets/widget_string.py | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index 3fd20ae1cb..92c7c8e57f 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -152,7 +152,7 @@ def on_delete(name=n): def test_gc_button(): deleted = False b = Button() - b.on_click(lambda x: setattr(b, "clicked", True)) + b.on_click(lambda x: setattr(x, "clicked", True)) def on_delete(): nonlocal deleted diff --git a/python/ipywidgets/ipywidgets/widgets/widget_button.py b/python/ipywidgets/ipywidgets/widgets/widget_button.py index a1f42d1e39..c3dae3967d 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_button.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_button.py @@ -14,8 +14,7 @@ from .widget_style import Style from .trait_types import Color, InstanceDict -import weakref -from traitlets import Unicode, Bool, CaselessStrEnum, Instance, validate, default +from traitlets import Unicode, Bool, CaselessStrEnum, validate @register class ButtonStyle(Style, CoreWidget): @@ -63,8 +62,7 @@ class Button(DOMWidget, CoreWidget): def __init__(self, **kwargs): super().__init__(**kwargs) self._click_handlers = CallbackDispatcher() - ref = weakref.ref(self) - self.on_msg(lambda w, c, b: ref()._handle_button_msg(w, c, b)) + self.on_msg(self._handle_button_msg) @validate('icon') def _validate_icon(self, proposal): diff --git a/python/ipywidgets/ipywidgets/widgets/widget_string.py b/python/ipywidgets/ipywidgets/widgets/widget_string.py index cfe3e4c9ea..e6f36bd8cc 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_string.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_string.py @@ -13,7 +13,6 @@ from .trait_types import Color, InstanceDict, TypedTuple from .utils import deprecation from traitlets import Unicode, Bool, Int -import weakref class _StringStyle(DescriptionStyle, CoreWidget): @@ -118,8 +117,7 @@ class Text(_String): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._submission_callbacks = CallbackDispatcher() - ref = weakref.ref(self) - self.on_msg(lambda w, c, b: ref()._handle_string_msg(w, c, b)) + self.on_msg(self._handle_string_msg) def _handle_string_msg(self, _, content, buffers): """Handle a msg from the front-end. From a634441fd2be42192ffb7d067a0b35a89013f670 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 4 May 2024 08:45:15 +1000 Subject: [PATCH 25/38] Using weakreference is now opt-in. --- .../ipywidgets/ipywidgets/widgets/__init__.py | 2 +- .../ipywidgets/widgets/tests/test_widget.py | 140 +++++++++++------- .../widgets/tests/test_widget_box.py | 35 +++-- .../ipywidgets/ipywidgets/widgets/widget.py | 24 ++- 4 files changed, 136 insertions(+), 65 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/__init__.py b/python/ipywidgets/ipywidgets/widgets/__init__.py index b90d3ee111..9fed2820af 100644 --- a/python/ipywidgets/ipywidgets/widgets/__init__.py +++ b/python/ipywidgets/ipywidgets/widgets/__init__.py @@ -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_weakrefence, disable_weakrefence from .domwidget import DOMWidget from .valuewidget import ValueWidget diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index 92c7c8e57f..4b02c296da 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -110,57 +110,97 @@ def test_widget_open(): button.get_view_spec() -def test_gc(): +def test_weakrefernce(): # Ensure the base instance of all widgets can be deleted / garbage collected. - classes = {} - for name, obj in ipw.__dict__.items(): - try: - if issubclass(obj, ipw.Widget): - classes[name] = obj - except Exception: - pass - assert classes, "No Widget classes were found!" - added = set() - collected = set() - objs = weakref.WeakSet() - options = ({}, {"options": [1, 2, 4]}, {"n_rows": 1}, {"options": ["A"]}) - for n, obj in classes.items(): - w = None - for kw in options: + ipw.enable_weakrefence() + try: + classes = {} + for name, obj in ipw.__dict__.items(): try: - w = obj(**kw) - w.comm - added.add(n) - break + if issubclass(obj, ipw.Widget): + classes[name] = obj except Exception: pass - if w: - def on_delete(name=n): - collected.add(name) - - weakref.finalize(w, on_delete) - objs.add(w) - # w should be the only strong ref to the widget. - # calling `del` should invoke its immediate deletion calling the `__del__` method. - del w - assert added, "No widgets were tested!" - gc.collect() - diff = added.difference(collected) - assert not diff, f"Widgets not garbage collected: {diff}" - - -def test_gc_button(): - deleted = False - b = Button() - b.on_click(lambda x: setattr(x, "clicked", True)) - - def on_delete(): - nonlocal deleted - deleted = True - - b.click() - assert getattr(b, "clicked") - weakref.finalize(b, on_delete) - del b - gc.collect() - assert deleted + assert classes, "No Widget classes were found!" + added = set() + collected = set() + objs = weakref.WeakSet() + options = ({}, {"options": [1, 2, 4]}, {"n_rows": 1}, {"options": ["A"]}) + for n, obj in classes.items(): + w = None + for kw in options: + try: + w = obj(**kw) + w.comm + added.add(n) + break + except Exception: + pass + if w: + def on_delete(name=n): + collected.add(name) + + weakref.finalize(w, on_delete) + objs.add(w) + # w should be the only strong ref to the widget. + # calling `del` should invoke its immediate deletion calling the `__del__` method. + del w + assert added, "No widgets were tested!" + gc.collect() + diff = added.difference(collected) + assert not diff, f"Widgets not garbage collected: {diff}" + finally: + ipw.disable_weakrefence() + + +@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_weakrefence() + assert b in widget._instances.values(), "Instances not transferred" + ipw.disable_weakrefence() + assert b in widget._instances.values(), "Instances not transferred" + ipw.enable_weakrefence() + 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_weakrefence() diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py index ce891c5c07..9d45d6776c 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py @@ -35,18 +35,27 @@ def test_construction_invalid_style(self): widgets.Box(box_style='invalid') - def test_gc(test): + def test_gc(self): + widgets.enable_weakrefence() # Test Box gc collected and children lifecycle managed. - deleted = False - b = widgets.VBox(children=[widgets.Button(description='button')]) - - def on_delete(): - nonlocal deleted - deleted = True - - weakref.finalize(b, on_delete) - del b - gc.collect() - assert deleted - + 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 + finally: + widgets.disable_weakrefence() \ No newline at end of file diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 3d31087343..6bf16103d6 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -43,7 +43,29 @@ def envset(name, default): JUPYTER_WIDGETS_ECHO = envset('JUPYTER_WIDGETS_ECHO', default=True) # for a discussion on using weak references see: # https://github.com/jupyter-widgets/ipywidgets/issues/1345 -_instances : typing.MutableMapping[str, "Widget"] = weakref.WeakValueDictionary() +_instances : typing.MutableMapping[str, "Widget"] = {} + +def enable_weakrefence(): + """Use a WeakValueDictionary instead of a standard dictionary to map + `comm_id` to `widget` for every widget instance. + + By default widgets are mapped using a standard dictionary. Use this feature + to permit widget garbage collection. + """ + global _instances + if not isinstance(_instances, weakref.WeakValueDictionary): + _instances = weakref.WeakValueDictionary(_instances) + +def disable_weakrefence(): + """Use a Dictionary to map `comm_id` to `widget` for every widget instance. + + Note: this is the default setting and maintains a strong reference to the + the widget preventing automatic garbage collection. If the close method + is called, the widget will remove itself enabling garbage collection. + """ + global _instances + if isinstance(_instances, weakref.WeakValueDictionary): + _instances = dict(_instances) def _widget_to_json(x, obj): if isinstance(x, Widget): From b1410eb7e7eb334b1c77d0be1d91b9ad46ae57e3 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 5 May 2024 09:30:22 +1000 Subject: [PATCH 26/38] Unify tooltips --- packages/base/src/widget.ts | 8 +++++++- packages/controls/src/widget_bool.ts | 2 +- packages/controls/src/widget_description.ts | 2 +- packages/controls/src/widget_selection.ts | 3 +-- packages/controls/src/widget_string.ts | 6 ++---- python/ipywidgets/ipywidgets/widgets/widget.py | 4 ++-- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/base/src/widget.ts b/packages/base/src/widget.ts index 6cb981fc02..1293874287 100644 --- a/packages/base/src/widget.ts +++ b/packages/base/src/widget.ts @@ -1095,7 +1095,7 @@ export class DOMWidgetView extends WidgetView { } updateTooltip(): void { - const title = this.model.get('tooltip'); + const title = this.tooltip; if (!title) { this.el.removeAttribute('title'); } else if (this.model.get('description').length === 0) { @@ -1103,6 +1103,12 @@ export class DOMWidgetView extends WidgetView { } } + get tooltip() { + return ( + this.model.get('tooltip') ?? (this.model.get('description') || 'null') + ); + } + /** * Update the DOM classes applied to an element, default to this.el. */ diff --git a/packages/controls/src/widget_bool.ts b/packages/controls/src/widget_bool.ts index 62ff0df2c3..50ca54a706 100644 --- a/packages/controls/src/widget_bool.ts +++ b/packages/controls/src/widget_bool.ts @@ -181,7 +181,7 @@ export class CheckboxView extends DescriptionView { updateTooltip(): void { super.updateTooltip(); if (!this.checkbox) return; // we might be constructing the parent - const title = this.model.get('tooltip') || ''; + const title = this.tooltip; this.checkbox.setAttribute('title', title); this.descriptionSpan.setAttribute('title', title); } diff --git a/packages/controls/src/widget_description.ts b/packages/controls/src/widget_description.ts index b1cc71a0a8..a809465277 100644 --- a/packages/controls/src/widget_description.ts +++ b/packages/controls/src/widget_description.ts @@ -99,7 +99,7 @@ export class DescriptionView extends DOMWidgetView { updateTooltip(): void { if (!this.label) return; - this.label.title = this.model.get('tooltip') || ''; + this.label.title = this.tooltip; } label: HTMLLabelElement; diff --git a/packages/controls/src/widget_selection.ts b/packages/controls/src/widget_selection.ts index 3b26881700..5eb37e2752 100644 --- a/packages/controls/src/widget_selection.ts +++ b/packages/controls/src/widget_selection.ts @@ -70,8 +70,7 @@ export class SelectionView extends DescriptionView { updateTooltip(): void { super.updateTooltip(); if (!this.listbox) return; // we might be constructing the parent - const title = this.model.get('tooltip') || ''; - this.listbox.setAttribute('title', title); + this.listbox.setAttribute('title', this.tooltip); } listbox: HTMLSelectElement; diff --git a/packages/controls/src/widget_string.ts b/packages/controls/src/widget_string.ts index d2cd6dcc18..3c708398d8 100644 --- a/packages/controls/src/widget_string.ts +++ b/packages/controls/src/widget_string.ts @@ -381,8 +381,7 @@ export class TextareaView extends StringView { updateTooltip(): void { super.updateTooltip(); if (!this.textbox) return; // we might be constructing the parent - const title = this.model.get('tooltip') || ''; - this.textbox.setAttribute('title', title); + this.textbox.setAttribute('title', this.tooltip); } events(): { [e: string]: string } { @@ -504,8 +503,7 @@ export class TextView extends StringView { updateTooltip(): void { super.updateTooltip(); if (!this.textbox) return; // we might be constructing the parent - const title = this.model.get('tooltip') || ''; - this.textbox.setAttribute('title', title); + this.textbox.setAttribute('title', this.tooltip); } update(options?: any): void { diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 6bf16103d6..7e846a1e0d 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -485,7 +485,7 @@ def _get_embed_state(self, drop_defaults=False): def get_view_spec(self): if not self._repr_mimebundle_: - msg = f"This widget is {self!r}" + msg = f"This widget is closed {self!r}" raise RuntimeError(msg) return {"version_major":2, "version_minor":0, "model_id":self._model_id or self.model_id} @@ -551,7 +551,7 @@ def open(self): # Accessing comm will load a default if it isn't already open. if self.comm is None: # None indicates the widget has been closed and shall not be opened. - msg = f"This widget is {self!r}." + msg = f"This widget is closed {self!r}." raise RuntimeError(msg) def _create_comm(self, comm_id=None): From 3972a9ddad7a153750464e536caef0769abd8d7b Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 5 May 2024 09:31:19 +1000 Subject: [PATCH 27/38] Added model_id to errorof get_model. --- packages/base-manager/src/manager-base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index 9ccddcc12a..098ff97a95 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -217,7 +217,7 @@ export abstract class ManagerBase implements IWidgetManager { async get_model(model_id: string): Promise { const modelPromise = this._models[model_id]; if (modelPromise === undefined) { - throw new Error('widget model not found'); + throw new Error(`widget model '${model_id}' not found`); } return modelPromise; } From f6fc921ef566fe51c6e1385611a95b1559231522 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 5 May 2024 12:01:01 +1000 Subject: [PATCH 28/38] Avoid attribute error on shutdown to do with _instances being None. --- python/ipywidgets/ipywidgets/widgets/widget.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 7e846a1e0d..99855e60b9 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -576,10 +576,13 @@ def _comm_changed(self, change): if change['old']: change['old'].on_msg(None) change['old'].close() - _instances.pop(change['old'].comm_id, None) + # On python shutdown _instances can be None + if isinstance(_instances, dict): + _instances.pop(change['old'].comm_id, None) if change['new']: self._model_id = change['new'].comm_id - _instances[change['new'].comm_id] = self + if isinstance(_instances, dict): + _instances[change['new'].comm_id] = self # prevent memory leaks by using a weak reference to self. ref = weakref.ref(self) From fc9fb8fb5d1c453fa3aa07ef3f9ed1a0cda07556 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 5 May 2024 12:02:25 +1000 Subject: [PATCH 29/38] Make Children validate raise TypeError for invalid items. --- python/ipywidgets/ipywidgets/widgets/widget_box.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index 3e01a1a61f..1101e08902 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -33,8 +33,18 @@ class Children(TraitType["tuple[Widget,...]", typing.Iterable[Widget]]): default_value = () - def validate(self, obj, value): - return tuple(v for v in value if isinstance(v, Widget) and v.comm) + def validate(self, obj:Box, value:typing.Iterable[Widget]): + invalid = [] + valid = [] + for v in value: + if isinstance(v, Widget) and v._repr_mimebundle_: + valid.append(v) + else: + invalid.append(v) + if invalid: + msg = f"Invalid items found: {invalid}" + raise TypeError(msg) + return tuple(valid) @register From dbd1b19ffd6162d79bbe4f23c5d9b69ae91ab8b3 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 5 May 2024 12:03:48 +1000 Subject: [PATCH 30/38] Parametrize test_weakreference. --- .../ipywidgets/widgets/tests/test_widget.py | 153 ++++++++++++------ 1 file changed, 101 insertions(+), 52 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index 4b02c296da..ddeeaff32d 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -3,22 +3,22 @@ """Test Widget.""" +import copy +import gc import inspect import weakref -import gc 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 -from ..widget_box import VBox -import copy -import ipywidgets as ipw def test_no_widget_view(): # ensure IPython shell is instantiated @@ -100,75 +100,124 @@ def test_widget_open(): 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'] + 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='This widget is closed'): + with pytest.raises(RuntimeError, match="This widget is closed"): button.open() - with pytest.raises(RuntimeError, match='This widget is closed'): + with pytest.raises(RuntimeError, match="This widget is closed"): button.get_view_spec() - -def test_weakrefernce(): + +@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", + ], +) +def test_weakreference(class_name): # Ensure the base instance of all widgets can be deleted / garbage collected. ipw.enable_weakrefence() + cls = getattr(ipw, class_name) + if class_name in ['SelectionRangeSlider', 'SelectionSlider']: + kwgs = {"options": [1, 2, 4]} + else: + kwgs = {} try: - classes = {} - for name, obj in ipw.__dict__.items(): - try: - if issubclass(obj, ipw.Widget): - classes[name] = obj - except Exception: - pass - assert classes, "No Widget classes were found!" - added = set() - collected = set() - objs = weakref.WeakSet() - options = ({}, {"options": [1, 2, 4]}, {"n_rows": 1}, {"options": ["A"]}) - for n, obj in classes.items(): - w = None - for kw in options: - try: - w = obj(**kw) - w.comm - added.add(n) - break - except Exception: - pass - if w: - def on_delete(name=n): - collected.add(name) - - weakref.finalize(w, on_delete) - objs.add(w) - # w should be the only strong ref to the widget. - # calling `del` should invoke its immediate deletion calling the `__del__` method. - del w - assert added, "No widgets were tested!" + 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. + del w gc.collect() - diff = added.difference(collected) - assert not diff, f"Widgets not garbage collected: {diff}" + assert deleted finally: ipw.disable_weakrefence() - -@pytest.mark.parametrize('weakref_enabled',[ True, False]) -def test_button_weakreference(weakref_enabled:bool): + +@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): + def my_click(self, b): nonlocal click_count click_count += 1 - b = TestButton(description='button') + b = TestButton(description="button") weakref.finalize(b, on_delete) b_ref = weakref.ref(b) assert b in widget._instances.values() @@ -200,7 +249,7 @@ def my_click (self, b): assert b_ref() in widget._instances.values() b_ref().close() gc.collect() - assert deleted, 'Closing should remove the last strong reference.' - + assert deleted, "Closing should remove the last strong reference." + finally: ipw.disable_weakrefence() From 70b88bc8387be9133cd54862858be09848fcf10e Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 5 May 2024 12:05:38 +1000 Subject: [PATCH 31/38] Add test_box_invalid_children convert TestBox to separate pytests. --- .../widgets/tests/test_widget_box.py | 117 ++++++++++-------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py index 9d45d6776c..cb9a2a87bc 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py @@ -3,59 +3,76 @@ import gc import weakref -from unittest import TestCase +import pytest from traitlets import TraitError import ipywidgets as widgets -class TestBox(TestCase): - - 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_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_gc(self): - widgets.enable_weakrefence() - # 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 - finally: - widgets.disable_weakrefence() - \ No newline at end of file +def test_box_construction(): + box = widgets.Box() + assert box.get_state()["children"] == [] + + +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_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_invalid_children(): + box = widgets.Box() + closed_button = widgets.Button(description="Closed") + closed_button.close() + + with pytest.raises(TypeError): + box.children = ["Not a widget"] + with pytest.raises(TypeError): + box.children = [closed_button] + with pytest.raises(TypeError): + box.children = [closed_button] + + +def test_box_gc(): + widgets.VBox._active_widgets + widgets.enable_weakrefence() + # 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_weakrefence() From bf3d0bc9d439d5ae6a182c35fece710cb1f4466e Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 5 May 2024 13:48:32 +1000 Subject: [PATCH 32/38] Remove dependency for istanbul-instrumenter-loader (un-maintained). --- packages/base-manager/package.json | 1 - packages/base/package.json | 1 - packages/controls/package.json | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/base-manager/package.json b/packages/base-manager/package.json index 42d5ddd2f1..5c10662f53 100644 --- a/packages/base-manager/package.json +++ b/packages/base-manager/package.json @@ -50,7 +50,6 @@ "chai": "^4.0.0", "chai-as-promised": "^7.0.0", "expect.js": "^0.3.1", - "istanbul-instrumenter-loader": "^3.0.1", "karma": "^6.3.3", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.3", diff --git a/packages/base/package.json b/packages/base/package.json index f21761f6ff..8c08d7d982 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -55,7 +55,6 @@ "chai": "^4.0.0", "chai-as-promised": "^7.0.0", "expect.js": "^0.3.1", - "istanbul-instrumenter-loader": "^3.0.1", "karma": "^6.3.3", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.3", diff --git a/packages/controls/package.json b/packages/controls/package.json index 0e044940bd..2eebb4981d 100644 --- a/packages/controls/package.json +++ b/packages/controls/package.json @@ -57,7 +57,6 @@ "chai": "^4.0.0", "css-loader": "^6.5.1", "expect.js": "^0.3.1", - "istanbul-instrumenter-loader": "^3.0.1", "karma": "^6.3.3", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.3", From 0d80e29a8db08f68e39a7d022a3c4ff828696c6c Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Mon, 6 May 2024 08:57:40 +1000 Subject: [PATCH 33/38] Fix Output tooltip issue & restored tooltip for Box. Removed _model_id and revised __rer__ for a closed widget. Added validate_mode for Box. --- packages/base/src/widget.ts | 4 +- packages/controls/src/widget_box.ts | 3 - packages/controls/src/widget_description.ts | 1 + .../ipywidgets/widgets/tests/test_widget.py | 4 +- .../widgets/tests/test_widget_box.py | 27 +- .../ipywidgets/ipywidgets/widgets/widget.py | 38 +- .../ipywidgets/widgets/widget_box.py | 43 ++- .../ipywidgets/widgets/widget_output.py | 1 + python/jupyterlab_widgets/package.json | 11 + yarn.lock | 350 ++---------------- 10 files changed, 92 insertions(+), 390 deletions(-) diff --git a/packages/base/src/widget.ts b/packages/base/src/widget.ts index 1293874287..b1c04f8624 100644 --- a/packages/base/src/widget.ts +++ b/packages/base/src/widget.ts @@ -1098,14 +1098,14 @@ export class DOMWidgetView extends WidgetView { const title = this.tooltip; if (!title) { this.el.removeAttribute('title'); - } else if (this.model.get('description').length === 0) { + } else if (!this.model.get('description')) { this.el.setAttribute('title', title); } } get tooltip() { return ( - this.model.get('tooltip') ?? (this.model.get('description') || 'null') + this.model.get('tooltip') ?? (this.model.get('description')) ); } diff --git a/packages/controls/src/widget_box.ts b/packages/controls/src/widget_box.ts index 889dc89092..2559d50d02 100644 --- a/packages/controls/src/widget_box.ts +++ b/packages/controls/src/widget_box.ts @@ -91,9 +91,6 @@ export class BoxView extends DOMWidgetView { this.set_box_style(); } - updateTooltip(): void { - return; - } update_children(): void { this.children_views ?.update(this.model.get('children')) diff --git a/packages/controls/src/widget_description.ts b/packages/controls/src/widget_description.ts index a809465277..67aec82c32 100644 --- a/packages/controls/src/widget_description.ts +++ b/packages/controls/src/widget_description.ts @@ -98,6 +98,7 @@ export class DescriptionView extends DOMWidgetView { } updateTooltip(): void { + super.updateTooltip(); if (!this.label) return; this.label.title = this.tooltip; } diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index ddeeaff32d..a932360c2a 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -104,9 +104,9 @@ def test_widget_open(): assert spec["model_id"] button.close() assert model_id not in widget._instances - with pytest.raises(RuntimeError, match="This widget is closed"): + with pytest.raises(RuntimeError, match="Widget is closed"): button.open() - with pytest.raises(RuntimeError, match="This widget is closed"): + with pytest.raises(RuntimeError, match="Widget is closed"): button.get_view_spec() diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py index cb9a2a87bc..e0ae3aa51e 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py @@ -36,17 +36,24 @@ def test_construction_invalid_style(): widgets.Box(box_style="invalid") -def test_box_invalid_children(): - box = widgets.Box() - closed_button = widgets.Button(description="Closed") +def test_box_validate_mode(): + slider = widgets.IntSlider() + closed_button = widgets.Button() closed_button.close() - - with pytest.raises(TypeError): - box.children = ["Not a widget"] - with pytest.raises(TypeError): - box.children = [closed_button] - with pytest.raises(TypeError): - box.children = [closed_button] + 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(): diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 99855e60b9..a356851dbc 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -69,23 +69,11 @@ def disable_weakrefence(): def _widget_to_json(x, obj): if isinstance(x, Widget): - if not x._repr_mimebundle_: - # a closed widget will not be found at the frontend so raise an error here. - msg = f"Widget is {x!r}" - raise RuntimeError(msg) - # _model_id provides faster access if its comm is already open. - return "IPY_MODEL_" + x._model_id or x.model_id + return f"IPY_MODEL_{x.model_id}" elif isinstance(x, (list, tuple)): return [_widget_to_json(v, obj) for v in x] elif isinstance(x, dict): return {k: _widget_to_json(v, obj) for k, v in x.items()} - elif hasattr(x, "_repr_mimebundle_"): - msg = ( - f"{x!r} is not a widget, but provides a `_repr_mimebundle_` attribute. " - "Support for direct `_repr_mimebundle_` has not been implemented yet. In the " - "meantime, it should be possible to view the object wih an `Output` widget." - ) - raise NotImplementedError(msg) else: return x @@ -326,7 +314,6 @@ class Widget(LoggingHasTraits): #------------------------------------------------------------------------- _widget_construction_callback = None _control_comm = None - _model_id = '' @_staticproperty def widgets(): @@ -484,10 +471,7 @@ def _get_embed_state(self, drop_defaults=False): return state def get_view_spec(self): - if not self._repr_mimebundle_: - msg = f"This widget is closed {self!r}" - raise RuntimeError(msg) - return {"version_major":2, "version_minor":0, "model_id":self._model_id or self.model_id} + return {"version_major":2, "version_minor":0, "model_id": self.model_id} #------------------------------------------------------------------------- # Traits @@ -548,11 +532,7 @@ def __del__(self): def open(self): """Open a comm to the frontend if one isn't already open.""" - # Accessing comm will load a default if it isn't already open. - if self.comm is None: - # None indicates the widget has been closed and shall not be opened. - msg = f"This widget is closed {self!r}." - raise RuntimeError(msg) + assert self.model_id def _create_comm(self, comm_id=None): """Open a new comm to the frontend.""" @@ -580,7 +560,6 @@ def _comm_changed(self, change): if isinstance(_instances, dict): _instances.pop(change['old'].comm_id, None) if change['new']: - self._model_id = change['new'].comm_id if isinstance(_instances, dict): _instances[change['new'].comm_id] = self @@ -595,8 +574,6 @@ def _handle_msg(msg): self_._show_traceback(self_._handle_msg, e) change['new'].on_msg(_handle_msg) - else: - self._model_id = '' @@ -605,6 +582,10 @@ def model_id(self): """Gets the model id of this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" + if not self._repr_mimebundle_: + # a closed widget will not be found at the frontend so raise an error here. + msg = f"Widget is closed: {self!r}" + raise RuntimeError(msg) return getattr(self.comm, "comm_id", None) @@ -756,8 +737,9 @@ def notify_change(self, change): super().notify_change(change) def __repr__(self): - rep = self._gen_repr_from_keys(self._repr_keys()) - return 'closed: ' + rep if not self._repr_mimebundle_ else rep + if not self._repr_mimebundle_: + return f'' + return self._gen_repr_from_keys(self._repr_keys()) #------------------------------------------------------------------------- # Support methods diff --git a/python/ipywidgets/ipywidgets/widgets/widget_box.py b/python/ipywidgets/ipywidgets/widgets/widget_box.py index 1101e08902..2eae86b2d9 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_box.py @@ -11,9 +11,8 @@ from __future__ import annotations import typing -import weakref -from traitlets import CaselessStrEnum, TraitType, Unicode +from traitlets import CaselessStrEnum, TraitError, TraitType, Unicode from .docutils import doc_subst from .domwidget import DOMWidget @@ -29,21 +28,33 @@ one of 'success', 'info', 'warning' or 'danger', or ''. Applies a predefined style to the box. Defaults to '', which applies no pre-defined style. + + validate_mode: str + one of 'raise', 'warning', error'. + How invalid children will be treated. + 'raise' will raise a trait error. + 'warning' and 'error' will log an error using box.log dropping + the invalid items from children. """ + class Children(TraitType["tuple[Widget,...]", typing.Iterable[Widget]]): default_value = () - def validate(self, obj:Box, value:typing.Iterable[Widget]): - invalid = [] - valid = [] + def validate(self, obj: Box, value: typing.Iterable[Widget]): + valid, invalid = [], [] for v in value: if isinstance(v, Widget) and v._repr_mimebundle_: valid.append(v) else: invalid.append(v) if invalid: - msg = f"Invalid items found: {invalid}" - raise TypeError(msg) + msg = f"Invalid or closed items found: {invalid}" + if obj.validate_mode == "log_warning": + obj.log.warning(msg) + elif obj.validate_mode == "log_error": + obj.log.error(msg) + else: + raise TraitError(msg) return tuple(valid) @@ -67,16 +78,15 @@ class Box(DOMWidget, CoreWidget): """ _model_name = Unicode('BoxModel').tag(sync=True) _view_name = Unicode('BoxView').tag(sync=True) - _children_handlers = weakref.WeakKeyDictionary() - - # Tooltip is not allowed for containers (override for DOMWidget). - tooltip = None + tooltip = Unicode('', allow_none=True, help='A tooltip caption.').tag(sync=True) + validate_mode = CaselessStrEnum(['raise', 'log_warning', 'log_error'], 'raise') # 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 = Children(help="List of widget children").tag( - sync=True, **widget_serialization) + children = Children(help='List of widget children').tag( + sync=True, **widget_serialization + ) box_style = CaselessStrEnum( values=['success', 'info', 'warning', 'danger', ''], default_value='', @@ -84,14 +94,9 @@ class Box(DOMWidget, CoreWidget): def __init__(self, children=(), **kwargs): if children: - kwargs['children'] = children + kwargs["children"] = children super().__init__(**kwargs) - - def close(self): - self.children = () - self._children_handlers.pop(self, None) - super().close() @register @doc_subst(_doc_snippets) diff --git a/python/ipywidgets/ipywidgets/widgets/widget_output.py b/python/ipywidgets/ipywidgets/widgets/widget_output.py index 150ac93471..c8729d9fed 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_output.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_output.py @@ -59,6 +59,7 @@ def func(): msg_id = Unicode('', help="Parent message id of messages to capture").tag(sync=True) outputs = TypedTuple(trait=Dict(), help="The output messages synced from the frontend.").tag(sync=True) + tooltip = Unicode('', allow_none=True, help="A tooltip caption.").tag(sync=True) __counter = 0 diff --git a/python/jupyterlab_widgets/package.json b/python/jupyterlab_widgets/package.json index 05ba093f55..4f9cac3ee6 100644 --- a/python/jupyterlab_widgets/package.json +++ b/python/jupyterlab_widgets/package.json @@ -92,5 +92,16 @@ "extension": true, "outputDir": "labextension", "schemaDir": "./schema" + }, + "optionalDependencies": { + "react": "^18.3.1" + }, + "peerDependencies": { + "react": "*" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } } } diff --git a/yarn.lock b/yarn.lock index 09ab1a9f2c..f2865d592d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -711,7 +711,6 @@ __metadata: chai: ^4.0.0 chai-as-promised: ^7.0.0 expect.js: ^0.3.1 - istanbul-instrumenter-loader: ^3.0.1 karma: ^6.3.3 karma-chrome-launcher: ^3.1.0 karma-coverage: ^2.0.3 @@ -753,7 +752,6 @@ __metadata: chai: ^4.0.0 chai-as-promised: ^7.0.0 expect.js: ^0.3.1 - istanbul-instrumenter-loader: ^3.0.1 jquery: ^3.1.1 karma: ^6.3.3 karma-chrome-launcher: ^3.1.0 @@ -797,7 +795,6 @@ __metadata: d3-color: ^3.0.1 d3-format: ^3.0.1 expect.js: ^0.3.1 - istanbul-instrumenter-loader: ^3.0.1 jquery: ^3.1.1 karma: ^6.3.3 karma-chrome-launcher: ^3.1.0 @@ -976,10 +973,19 @@ __metadata: jquery: ^3.1.1 npm-run-all: ^4.1.5 prettier: ^2.3.2 + react: ^18.3.1 rimraf: ^3.0.2 semver: ^7.3.5 source-map-loader: ^4.0.1 typescript: ~4.9.4 + peerDependencies: + react: "*" + dependenciesMeta: + react: + optional: true + peerDependenciesMeta: + react: + optional: true languageName: unknown linkType: soft @@ -4466,18 +4472,6 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^5.0.0": - version: 5.5.2 - resolution: "ajv@npm:5.5.2" - dependencies: - co: ^4.6.0 - fast-deep-equal: ^1.0.0 - fast-json-stable-stringify: ^2.0.0 - json-schema-traverse: ^0.3.0 - checksum: a69645c843e1676b0ae1c5192786e546427f808f386d26127c6585479378066c64341ceec0b127b6789d79628e71d2a732d402f575b98f9262db230d7b715a94 - languageName: node - linkType: hard - "ajv@npm:^6.10.0, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -4525,13 +4519,6 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^2.0.0": - version: 2.1.1 - resolution: "ansi-regex@npm:2.1.1" - checksum: 190abd03e4ff86794f338a31795d262c1dfe8c91f7e01d04f13f646f1dcb16c5800818f886047876f1272f065570ab86b24b99089f8b68a0e11ff19aed4ca8f1 - languageName: node - linkType: hard - "ansi-regex@npm:^3.0.0": version: 3.0.1 resolution: "ansi-regex@npm:3.0.1" @@ -4560,13 +4547,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^2.2.1": - version: 2.2.1 - resolution: "ansi-styles@npm:2.2.1" - checksum: ebc0e00381f2a29000d1dac8466a640ce11943cef3bda3cd0020dc042e31e1058ab59bf6169cd794a54c3a7338a61ebc404b7c91e004092dd20e028c432c9c2c - languageName: node - linkType: hard - "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -4823,43 +4803,7 @@ __metadata: languageName: node linkType: hard -"babel-code-frame@npm:^6.26.0": - version: 6.26.0 - resolution: "babel-code-frame@npm:6.26.0" - dependencies: - chalk: ^1.1.3 - esutils: ^2.0.2 - js-tokens: ^3.0.2 - checksum: 9410c3d5a921eb02fa409675d1a758e493323a49e7b9dddb7a2a24d47e61d39ab1129dd29f9175836eac9ce8b1d4c0a0718fcdc57ce0b865b529fd250dbab313 - languageName: node - linkType: hard - -"babel-generator@npm:^6.18.0": - version: 6.26.1 - resolution: "babel-generator@npm:6.26.1" - dependencies: - babel-messages: ^6.23.0 - babel-runtime: ^6.26.0 - babel-types: ^6.26.0 - detect-indent: ^4.0.0 - jsesc: ^1.3.0 - lodash: ^4.17.4 - source-map: ^0.5.7 - trim-right: ^1.0.1 - checksum: 5397f4d4d1243e7157e3336be96c10fcb1f29f73bf2d9842229c71764d9a6431397d249483a38c4d8b1581459e67be4df6f32d26b1666f02d0f5bfc2c2f25193 - languageName: node - linkType: hard - -"babel-messages@npm:^6.23.0": - version: 6.23.0 - resolution: "babel-messages@npm:6.23.0" - dependencies: - babel-runtime: ^6.22.0 - checksum: c8075c17587a33869e1a5bd0a5b73bbe395b68188362dacd5418debbc7c8fd784bcd3295e81ee7e410dc2c2655755add6af03698c522209f6a68334c15e6d6ca - languageName: node - linkType: hard - -"babel-runtime@npm:^6.22.0, babel-runtime@npm:^6.23.0, babel-runtime@npm:^6.26.0": +"babel-runtime@npm:^6.23.0": version: 6.26.0 resolution: "babel-runtime@npm:6.26.0" dependencies: @@ -4869,57 +4813,6 @@ __metadata: languageName: node linkType: hard -"babel-template@npm:^6.16.0": - version: 6.26.0 - resolution: "babel-template@npm:6.26.0" - dependencies: - babel-runtime: ^6.26.0 - babel-traverse: ^6.26.0 - babel-types: ^6.26.0 - babylon: ^6.18.0 - lodash: ^4.17.4 - checksum: 028dd57380f09b5641b74874a19073c53c4fb3f1696e849575aae18f8c80eaf21db75209057db862f3b893ce2cd9b795d539efa591b58f4a0fb011df0a56fbed - languageName: node - linkType: hard - -"babel-traverse@npm:^6.18.0, babel-traverse@npm:^6.26.0": - version: 6.26.0 - resolution: "babel-traverse@npm:6.26.0" - dependencies: - babel-code-frame: ^6.26.0 - babel-messages: ^6.23.0 - babel-runtime: ^6.26.0 - babel-types: ^6.26.0 - babylon: ^6.18.0 - debug: ^2.6.8 - globals: ^9.18.0 - invariant: ^2.2.2 - lodash: ^4.17.4 - checksum: fca037588d2791ae0409f1b7aa56075b798699cccc53ea04d82dd1c0f97b9e7ab17065f7dd3ecd69101d7874c9c8fd5e0f88fa53abbae1fe94e37e6b81ebcb8d - languageName: node - linkType: hard - -"babel-types@npm:^6.18.0, babel-types@npm:^6.26.0": - version: 6.26.0 - resolution: "babel-types@npm:6.26.0" - dependencies: - babel-runtime: ^6.26.0 - esutils: ^2.0.2 - lodash: ^4.17.4 - to-fast-properties: ^1.0.3 - checksum: d16b0fa86e9b0e4c2623be81d0a35679faff24dd2e43cde4ca58baf49f3e39415a011a889e6c2259ff09e1228e4c3a3db6449a62de59e80152fe1ce7398fde76 - languageName: node - linkType: hard - -"babylon@npm:^6.18.0": - version: 6.18.0 - resolution: "babylon@npm:6.18.0" - bin: - babylon: ./bin/babylon.js - checksum: 0777ae0c735ce1cbfc856d627589ed9aae212b84fb0c03c368b55e6c5d3507841780052808d0ad46e18a2ba516e93d55eeed8cd967f3b2938822dfeccfb2a16d - languageName: node - linkType: hard - "backbone@npm:1.4.0": version: 1.4.0 resolution: "backbone@npm:1.4.0" @@ -5356,19 +5249,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^1.1.3": - version: 1.1.3 - resolution: "chalk@npm:1.1.3" - dependencies: - ansi-styles: ^2.2.1 - escape-string-regexp: ^1.0.2 - has-ansi: ^2.0.0 - strip-ansi: ^3.0.0 - supports-color: ^2.0.0 - checksum: 9d2ea6b98fc2b7878829eec223abcf404622db6c48396a9b9257f6d0ead2acf18231ae368d6a664a83f272b0679158da12e97b5229f794939e555cc574478acd - languageName: node - linkType: hard - "chalk@npm:^2.0.0, chalk@npm:^2.0.1, chalk@npm:^2.1.0, chalk@npm:^2.3.0, chalk@npm:^2.4.1": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -5591,13 +5471,6 @@ __metadata: languageName: node linkType: hard -"co@npm:^4.6.0": - version: 4.6.0 - resolution: "co@npm:4.6.0" - checksum: 5210d9223010eb95b29df06a91116f2cf7c8e0748a9013ed853b53f362ea0e822f1e5bb054fb3cefc645239a4cf966af1f6133a3b43f40d591f3b68ed6cf0510 - languageName: node - linkType: hard - "codemirror@npm:^5.48.0": version: 5.65.13 resolution: "codemirror@npm:5.65.13" @@ -5987,7 +5860,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^1.5.0, convert-source-map@npm:^1.7.0": +"convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 @@ -6259,7 +6132,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2.6.9, debug@npm:^2.6.8": +"debug@npm:2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -6460,15 +6333,6 @@ __metadata: languageName: node linkType: hard -"detect-indent@npm:^4.0.0": - version: 4.0.0 - resolution: "detect-indent@npm:4.0.0" - dependencies: - repeating: ^2.0.0 - checksum: 328f273915c1610899bc7d4784ce874413d0a698346364cd3ee5d79afba1c5cf4dbc97b85a801e20f4d903c0598bd5096af32b800dfb8696b81464ccb3dfda2c - languageName: node - linkType: hard - "detect-indent@npm:^5.0.0": version: 5.0.0 resolution: "detect-indent@npm:5.0.0" @@ -6997,7 +6861,7 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": +"escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 @@ -7304,13 +7168,6 @@ __metadata: languageName: node linkType: hard -"fast-deep-equal@npm:^1.0.0": - version: 1.1.0 - resolution: "fast-deep-equal@npm:1.1.0" - checksum: 69b4c9534d9805f13a341aa72f69641d0b9ae3cc8beb25c64e68a257241c7bb34370266db27ae4fc3c4da0518448c01a5f587a096a211471c86a38facd9a1486 - languageName: node - linkType: hard - "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -8083,13 +7940,6 @@ __metadata: languageName: node linkType: hard -"globals@npm:^9.18.0": - version: 9.18.0 - resolution: "globals@npm:9.18.0" - checksum: e9c066aecfdc5ea6f727344a4246ecc243aaf66ede3bffee10ddc0c73351794c25e727dd046090dcecd821199a63b9de6af299a6e3ba292c8b22f0a80ea32073 - languageName: node - linkType: hard - "globalthis@npm:^1.0.3": version: 1.0.3 resolution: "globalthis@npm:1.0.3" @@ -8247,15 +8097,6 @@ __metadata: languageName: node linkType: hard -"has-ansi@npm:^2.0.0": - version: 2.0.0 - resolution: "has-ansi@npm:2.0.0" - dependencies: - ansi-regex: ^2.0.0 - checksum: 1b51daa0214440db171ff359d0a2d17bc20061164c57e76234f614c91dbd2a79ddd68dfc8ee73629366f7be45a6df5f2ea9de83f52e1ca24433f2cc78c35d8ec - languageName: node - linkType: hard - "has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": version: 1.0.2 resolution: "has-bigints@npm:1.0.2" @@ -8768,15 +8609,6 @@ __metadata: languageName: node linkType: hard -"invariant@npm:^2.2.2": - version: 2.2.4 - resolution: "invariant@npm:2.2.4" - dependencies: - loose-envify: ^1.0.0 - checksum: cc3182d793aad82a8d1f0af697b462939cb46066ec48bbf1707c150ad5fad6406137e91a262022c269702e01621f35ef60269f6c0d7fd178487959809acdfb14 - languageName: node - linkType: hard - "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -8896,13 +8728,6 @@ __metadata: languageName: node linkType: hard -"is-finite@npm:^1.0.0": - version: 1.1.0 - resolution: "is-finite@npm:1.1.0" - checksum: 532b97ed3d03e04c6bd203984d9e4ba3c0c390efee492bad5d1d1cd1802a68ab27adbd3ef6382f6312bed6c8bb1bd3e325ea79a8dc8fe080ed7a06f5f97b93e7 - languageName: node - linkType: hard - "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -9198,27 +9023,6 @@ __metadata: languageName: node linkType: hard -"istanbul-instrumenter-loader@npm:^3.0.1": - version: 3.0.1 - resolution: "istanbul-instrumenter-loader@npm:3.0.1" - dependencies: - convert-source-map: ^1.5.0 - istanbul-lib-instrument: ^1.7.3 - loader-utils: ^1.1.0 - schema-utils: ^0.3.0 - peerDependencies: - webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 - checksum: 6b2eb9987f79dd451c43e0fcc6fa77bf0f7ac91f3237b7833a07ad6f35e15a6bff579e943edfc2dee203408b6c3a2b4b11f3028b8628cb7304df3decc7552831 - languageName: node - linkType: hard - -"istanbul-lib-coverage@npm:^1.2.1": - version: 1.2.1 - resolution: "istanbul-lib-coverage@npm:1.2.1" - checksum: 72bfeaa9212f5a6abb243cbce4933712599ba9a6fbdee819f4f5a4cf87ed15cb92772fcab219e93c3712c578774d6d8e54084440423356b3da5d9f8ecaba9888 - languageName: node - linkType: hard - "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-lib-coverage@npm:3.2.0" @@ -9226,21 +9030,6 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-instrument@npm:^1.7.3": - version: 1.10.2 - resolution: "istanbul-lib-instrument@npm:1.10.2" - dependencies: - babel-generator: ^6.18.0 - babel-template: ^6.16.0 - babel-traverse: ^6.18.0 - babel-types: ^6.18.0 - babylon: ^6.18.0 - istanbul-lib-coverage: ^1.2.1 - semver: ^5.3.0 - checksum: c299d73820b0ac93d1c53f436181da09579083dc4a0febadbda93f598f9a5591fe4888c3071a913eede36148d6481fdf163fa0b6ec7156fffe2a95cff965fc51 - languageName: node - linkType: hard - "istanbul-lib-instrument@npm:^5.1.0": version: 5.2.1 resolution: "istanbul-lib-instrument@npm:5.2.1" @@ -9345,13 +9134,6 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^3.0.2": - version: 3.0.2 - resolution: "js-tokens@npm:3.0.2" - checksum: ff24cf90e6e4ac446eba56e604781c1aaf3bdaf9b13a00596a0ebd972fa3b25dc83c0f0f67289c33252abb4111e0d14e952a5d9ffb61f5c22532d555ebd8d8a9 - languageName: node - linkType: hard - "js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" @@ -9382,15 +9164,6 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:^1.3.0": - version: 1.3.0 - resolution: "jsesc@npm:1.3.0" - bin: - jsesc: bin/jsesc - checksum: 9384cc72bf8ef7f2eb75fea64176b8b0c1c5e77604854c72cb4670b7072e112e3baaa69ef134be98cb078834a7812b0bfe676ad441ccd749a59427f5ed2127f1 - languageName: node - linkType: hard - "jsesc@npm:^2.5.1": version: 2.5.2 resolution: "jsesc@npm:2.5.2" @@ -9441,13 +9214,6 @@ __metadata: languageName: node linkType: hard -"json-schema-traverse@npm:^0.3.0": - version: 0.3.1 - resolution: "json-schema-traverse@npm:0.3.1" - checksum: a685c36222023471c25c86cddcff506306ecb8f8941922fd356008419889c41c38e1c16d661d5499d0a561b34f417693e9bb9212ba2b2b2f8f8a345a49e4ec1a - languageName: node - linkType: hard - "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -9490,17 +9256,6 @@ __metadata: languageName: node linkType: hard -"json5@npm:^1.0.1": - version: 1.0.2 - resolution: "json5@npm:1.0.2" - dependencies: - minimist: ^1.2.0 - bin: - json5: lib/cli.js - checksum: 866458a8c58a95a49bef3adba929c625e82532bcff1fe93f01d29cb02cac7c3fe1f4b79951b7792c2da9de0b32871a8401a6e3c5b36778ad852bf5b8a61165d7 - languageName: node - linkType: hard - "json5@npm:^2.1.2, json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" @@ -10016,17 +9771,6 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:^1.1.0": - version: 1.4.2 - resolution: "loader-utils@npm:1.4.2" - dependencies: - big.js: ^5.2.2 - emojis-list: ^3.0.0 - json5: ^1.0.1 - checksum: eb6fb622efc0ffd1abdf68a2022f9eac62bef8ec599cf8adb75e94d1d338381780be6278534170e99edc03380a6d29bc7eb1563c89ce17c5fed3a0b17f1ad804 - languageName: node - linkType: hard - "loader-utils@npm:^2.0.0": version: 2.0.4 resolution: "loader-utils@npm:2.0.4" @@ -10208,7 +9952,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -12937,6 +12681,15 @@ __metadata: languageName: node linkType: hard +"react@npm:^18.3.1": + version: 18.3.1 + resolution: "react@npm:18.3.1" + dependencies: + loose-envify: ^1.1.0 + checksum: a27bcfa8ff7c15a1e50244ad0d0c1cb2ad4375eeffefd266a64889beea6f6b64c4966c9b37d14ee32d6c9fcd5aa6ba183b6988167ab4d127d13e7cb5b386a376 + languageName: node + linkType: hard + "read-cache@npm:^1.0.0": version: 1.0.0 resolution: "read-cache@npm:1.0.0" @@ -13195,15 +12948,6 @@ __metadata: languageName: node linkType: hard -"repeating@npm:^2.0.0": - version: 2.0.1 - resolution: "repeating@npm:2.0.1" - dependencies: - is-finite: ^1.0.0 - checksum: d2db0b69c5cb0c14dd750036e0abcd6b3c3f7b2da3ee179786b755cf737ca15fa0fff417ca72de33d6966056f4695440e680a352401fc02c95ade59899afbdd0 - languageName: node - linkType: hard - "request@npm:2.88.2": version: 2.88.2 resolution: "request@npm:2.88.2" @@ -13545,15 +13289,6 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:^0.3.0": - version: 0.3.0 - resolution: "schema-utils@npm:0.3.0" - dependencies: - ajv: ^5.0.0 - checksum: 441fa4bd4900afb19eb9da1d8d6271056b71ce3d8b1b73bbece791de1d4c90ac7e97ffc9787607aa53611aaf2996711af7c18ba8669f06b084b218cab1e701e3 - languageName: node - linkType: hard - "schema-utils@npm:^2.7.0": version: 2.7.1 resolution: "schema-utils@npm:2.7.1" @@ -13606,7 +13341,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.3.0, semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.6.0": +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.6.0": version: 5.7.1 resolution: "semver@npm:5.7.1" bin: @@ -14085,13 +13820,6 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.5.7": - version: 0.5.7 - resolution: "source-map@npm:0.5.7" - checksum: 5dc2043b93d2f194142c7f38f74a24670cd7a0063acdaf4bf01d2964b402257ae843c2a8fa822ad5b71013b5fcafa55af7421383da919752f22ff488bc553f4d - languageName: node - linkType: hard - "source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" @@ -14336,15 +14064,6 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^3.0.0": - version: 3.0.1 - resolution: "strip-ansi@npm:3.0.1" - dependencies: - ansi-regex: ^2.0.0 - checksum: 9b974de611ce5075c70629c00fa98c46144043db92ae17748fb780f706f7a789e9989fd10597b7c2053ae8d1513fd707816a91f1879b2f71e6ac0b6a863db465 - languageName: node - linkType: hard - "strip-ansi@npm:^4.0.0": version: 4.0.0 resolution: "strip-ansi@npm:4.0.0" @@ -14445,13 +14164,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^2.0.0": - version: 2.0.0 - resolution: "supports-color@npm:2.0.0" - checksum: 602538c5812b9006404370b5a4b885d3e2a1f6567d314f8b4a41974ffe7d08e525bf92ae0f9c7030e3b4c78e4e34ace55d6a67a74f1571bc205959f5972f88f0 - languageName: node - linkType: hard - "supports-color@npm:^5.3.0, supports-color@npm:^5.4.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -14635,13 +14347,6 @@ __metadata: languageName: node linkType: hard -"to-fast-properties@npm:^1.0.3": - version: 1.0.3 - resolution: "to-fast-properties@npm:1.0.3" - checksum: bd0abb58c4722851df63419de3f6d901d5118f0440d3f71293ed776dd363f2657edaaf2dc470e3f6b7b48eb84aa411193b60db8a4a552adac30de9516c5cc580 - languageName: node - linkType: hard - "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -14712,13 +14417,6 @@ __metadata: languageName: node linkType: hard -"trim-right@npm:^1.0.1": - version: 1.0.1 - resolution: "trim-right@npm:1.0.1" - checksum: 9120af534e006a7424a4f9358710e6e707887b6ccf7ea69e50d6ac6464db1fe22268400def01752f09769025d480395159778153fb98d4a2f6f40d4cf5d4f3b6 - languageName: node - linkType: hard - "tsconfig-paths@npm:^4.1.2": version: 4.2.0 resolution: "tsconfig-paths@npm:4.2.0" From 35be63aac27bac06ead02d4bf13686cce5310fb3 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 11 May 2024 09:32:26 +1000 Subject: [PATCH 34/38] Modify test_weakreference for not enabled (closing should also make widget available for gc). --- .../ipywidgets/widgets/tests/test_widget.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index a932360c2a..4a4296e819 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -178,9 +178,11 @@ def test_widget_open(): "Widget", ], ) -def test_weakreference(class_name): +@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. - ipw.enable_weakrefence() + if enable_weakref: + ipw.enable_weakrefence() cls = getattr(ipw, class_name) if class_name in ['SelectionRangeSlider', 'SelectionSlider']: kwgs = {"options": [1, 2, 4]} @@ -195,11 +197,14 @@ def on_delete(): 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: - ipw.disable_weakrefence() + if enable_weakref: + ipw.disable_weakrefence() @pytest.mark.parametrize("weakref_enabled", [True, False]) From a776397d1a3b1bfab0142d4fd55e2786b5e65b80 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 11 May 2024 09:59:58 +1000 Subject: [PATCH 35/38] Fix spelling of enable_weakreference and disable_weakreference. --- python/ipywidgets/ipywidgets/widgets/__init__.py | 2 +- .../ipywidgets/widgets/tests/test_widget.py | 12 ++++++------ .../ipywidgets/widgets/tests/test_widget_box.py | 4 ++-- python/ipywidgets/ipywidgets/widgets/widget.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/__init__.py b/python/ipywidgets/ipywidgets/widgets/__init__.py index 9fed2820af..0951bad905 100644 --- a/python/ipywidgets/ipywidgets/widgets/__init__.py +++ b/python/ipywidgets/ipywidgets/widgets/__init__.py @@ -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, enable_weakrefence, disable_weakrefence +from .widget import Widget, CallbackDispatcher, register, widget_serialization, enable_weakreference, disable_weakreference from .domwidget import DOMWidget from .valuewidget import ValueWidget diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py index 4a4296e819..34fd9402a2 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget.py @@ -182,7 +182,7 @@ def test_widget_open(): def test_weakreference(class_name, enable_weakref): # Ensure the base instance of all widgets can be deleted / garbage collected. if enable_weakref: - ipw.enable_weakrefence() + ipw.enable_weakreference() cls = getattr(ipw, class_name) if class_name in ['SelectionRangeSlider', 'SelectionSlider']: kwgs = {"options": [1, 2, 4]} @@ -204,7 +204,7 @@ def on_delete(): assert deleted finally: if enable_weakref: - ipw.disable_weakrefence() + ipw.disable_weakreference() @pytest.mark.parametrize("weakref_enabled", [True, False]) @@ -234,11 +234,11 @@ def my_click(self, b): assert click_count == 1 if weakref_enabled: - ipw.enable_weakrefence() + ipw.enable_weakreference() assert b in widget._instances.values(), "Instances not transferred" - ipw.disable_weakrefence() + ipw.disable_weakreference() assert b in widget._instances.values(), "Instances not transferred" - ipw.enable_weakrefence() + ipw.enable_weakreference() assert b in widget._instances.values(), "Instances not transferred" b.click() @@ -257,4 +257,4 @@ def my_click(self, b): assert deleted, "Closing should remove the last strong reference." finally: - ipw.disable_weakrefence() + ipw.disable_weakreference() diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py index e0ae3aa51e..5d50324d08 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_box.py @@ -58,7 +58,7 @@ def test_box_validate_mode(): def test_box_gc(): widgets.VBox._active_widgets - widgets.enable_weakrefence() + widgets.enable_weakreference() # Test Box gc collected and children lifecycle managed. try: deleted = False @@ -82,4 +82,4 @@ def on_delete(): assert deleted widgets.VBox._active_widgets finally: - widgets.disable_weakrefence() + widgets.disable_weakreference() diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index a356851dbc..7acb00724f 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -45,7 +45,7 @@ def envset(name, default): # https://github.com/jupyter-widgets/ipywidgets/issues/1345 _instances : typing.MutableMapping[str, "Widget"] = {} -def enable_weakrefence(): +def enable_weakreference(): """Use a WeakValueDictionary instead of a standard dictionary to map `comm_id` to `widget` for every widget instance. @@ -56,7 +56,7 @@ def enable_weakrefence(): if not isinstance(_instances, weakref.WeakValueDictionary): _instances = weakref.WeakValueDictionary(_instances) -def disable_weakrefence(): +def disable_weakreference(): """Use a Dictionary to map `comm_id` to `widget` for every widget instance. Note: this is the default setting and maintains a strong reference to the From 7741dbbf0db6db8451c1f8f7ac5611775908d005 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 11 May 2024 14:26:22 +1000 Subject: [PATCH 36/38] Revert Link (drop _all_links). Added type annotations for link and dlink. --- .../ipywidgets/ipywidgets/widgets/widget_link.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget_link.py b/python/ipywidgets/ipywidgets/widgets/widget_link.py index 5916c1c454..93bda516ab 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_link.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_link.py @@ -6,6 +6,8 @@ Propagate changes between widgets on the javascript side. """ +from __future__ import annotations + from .widget import Widget, register, widget_serialization from .widget_core import CoreWidget @@ -51,25 +53,19 @@ class Link(CoreWidget): source: a (Widget, 'trait_name') tuple for the source trait target: a (Widget, 'trait_name') tuple that should be updated """ - # maintain a set of links to keep them alive - _all_links = set() _model_name = Unicode('LinkModel').tag(sync=True) target = WidgetTraitTuple(help="The target (widget, 'trait_name') pair").tag(sync=True, **widget_serialization) source = WidgetTraitTuple(help="The source (widget, 'trait_name') pair").tag(sync=True, **widget_serialization) - - def __init__(self, source, target, **kwargs): + + def __init__(self, source: tuple[Widget, str], target: tuple[Widget, str], **kwargs): super().__init__(source=source, target=target, **kwargs) - self._all_links.add(self) - def close(self): - self._all_links.discard(self) - super().close() # for compatibility with traitlet links def unlink(self): self.close() -def jslink(attr1, attr2): +def jslink(attr1: tuple[Widget, str], attr2: tuple[Widget, str]): """Link two widget attributes on the frontend so they remain in sync. The link is created in the front-end and does not rely on a roundtrip @@ -99,7 +95,7 @@ class DirectionalLink(Link): _model_name = Unicode('DirectionalLinkModel').tag(sync=True) -def jsdlink(source, target): +def jsdlink(source: tuple[Widget, str], target: tuple[Widget, str]): """Link a source widget attribute with a target widget attribute. The link is created in the front-end and does not rely on a roundtrip From 1c4d1adf2c40fe85c58412679e32e5f9fa1e3d23 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 19 May 2024 23:43:45 +1000 Subject: [PATCH 37/38] Changes to jupyterlab_widgets plugin and manager to make the KernelWidgetManager a singleton on a per kernel basis. Other fixes to make widgets more accessible. --- packages/base-manager/src/manager-base.ts | 8 +- python/jupyterlab_widgets/src/manager.ts | 256 +++++++++++++++++++--- python/jupyterlab_widgets/src/output.ts | 31 +-- python/jupyterlab_widgets/src/plugin.ts | 87 ++------ python/jupyterlab_widgets/src/renderer.ts | 21 +- 5 files changed, 271 insertions(+), 132 deletions(-) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index 098ff97a95..2313b68e35 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -214,7 +214,11 @@ export abstract class ManagerBase implements IWidgetManager { * * If you would like to synchronously test if a model exists, use .has_model(). */ - async get_model(model_id: string): Promise { + async get_model(model_id: string, timeout = 1000): Promise { + let count = 0; + while (!this._models[model_id] && count++ < timeout / 10) { + await sleep(10); + } const modelPromise = this._models[model_id]; if (modelPromise === undefined) { throw new Error(`widget model '${model_id}' not found`); @@ -919,6 +923,8 @@ export function serialize_state( return { version_major: 2, version_minor: 0, state: state }; } +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + namespace Private { /** * Data promised when a comm info request resolves. diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index fec2cb3e4e..c4870099d0 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -2,20 +2,20 @@ // Distributed under the terms of the Modified BSD License. import { - shims, + ExportData, + ExportMap, + ICallbacks, IClassicComm, IWidgetRegistryData, - ExportMap, - ExportData, WidgetModel, WidgetView, - ICallbacks, + shims, } from '@jupyter-widgets/base'; import { + IStateOptions, ManagerBase, serialize_state, - IStateOptions, } from '@jupyter-widgets/base-manager'; import { IDisposable } from '@lumino/disposable'; @@ -26,7 +26,18 @@ import { INotebookModel } from '@jupyterlab/notebook'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { Kernel, KernelMessage, Session } from '@jupyterlab/services'; +import { ObservableList, ObservableMap } from '@jupyterlab/observables'; + +import * as nbformat from '@jupyterlab/nbformat'; + +import { ILoggerRegistry, LogLevel } from '@jupyterlab/logconsole'; + +import { + Kernel, + KernelConnection, + KernelMessage, + Session, +} from '@jupyterlab/services'; import { DocumentRegistry } from '@jupyterlab/docregistry'; @@ -36,6 +47,11 @@ import { valid } from 'semver'; import { SemVerCache } from './semvercache'; +import Backbone from 'backbone'; + +import * as base from '@jupyter-widgets/base'; +import { WidgetRenderer } from './renderer'; + /** * The mime type for a widget view. */ @@ -330,23 +346,39 @@ export abstract class LabWidgetManager this, KernelMessage.IIOPubMessage >(this); + static WIDGET_REGISTRY = new ObservableList(); } /** - * A widget manager that returns Lumino widgets. + * A singleton widget manager per kernel for the lifecycle of the kernel. */ export class KernelWidgetManager extends LabWidgetManager { constructor( kernel: Kernel.IKernelConnection, rendermime: IRenderMimeRegistry ) { + const instance = Private.kernelWidgetManagers.get(kernel.id); + if (instance) { + instance.attachToRendermime(rendermime); + return instance; + } super(rendermime); - this._kernel = kernel; - - kernel.statusChanged.connect((sender, args) => { + this.attachToRendermime(rendermime); + Private.kernelWidgetManagers.set(kernel.id, this); + this._kernel = new KernelConnection({ model: kernel.model }); + this.loadCustomWidgetDefinitions(); + LabWidgetManager.WIDGET_REGISTRY.changed.connect(() => + this.loadCustomWidgetDefinitions() + ); + this._kernel.registerCommTarget( + this.comm_target_name, + this._handleCommOpen + ); + + this._kernel.statusChanged.connect((sender, args) => { this._handleKernelStatusChange(args); }); - kernel.connectionStatusChanged.connect((sender, args) => { + this._kernel.connectionStatusChanged.connect((sender, args) => { this._handleKernelConnectionStatusChange(args); }); @@ -405,24 +437,50 @@ export class KernelWidgetManager extends LabWidgetManager { return this._kernel; } + loadCustomWidgetDefinitions() { + for (const data of LabWidgetManager.WIDGET_REGISTRY) { + this.register(data); + } + } + + filterModelState(serialized_state: any): any { + return this.filterExistingModelState(serialized_state); + } + + attachToRendermime(rendermime: IRenderMimeRegistry) { + rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); + rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_VIEW_MIMETYPE], + createRenderer: (options) => new WidgetRenderer(options, this), + }, + -10 + ); + } + private _kernel: Kernel.IKernelConnection; + protected _kernelRestoreInProgress = false; } /** - * A widget manager that returns phosphor widgets. + * Monitor kernel of the Context swapping the kernel manager on demand. + * A better name would be `NotebookManagerSwitcher'. */ -export class WidgetManager extends LabWidgetManager { +export class WidgetManager extends Backbone.Model implements IDisposable { constructor( context: DocumentRegistry.IContext, rendermime: IRenderMimeRegistry, settings: WidgetManager.Settings ) { - super(rendermime); + super(); + this._rendermime = rendermime; this._context = context; + this._settings = settings; - context.sessionContext.kernelChanged.connect((sender, args) => { - this._handleKernelChanged(args); - }); + context.sessionContext.kernelChanged.connect((sender, args) => + this.updateWidgetManager() + ); context.sessionContext.statusChanged.connect((sender, args) => { this._handleKernelStatusChange(args); @@ -432,17 +490,11 @@ export class WidgetManager extends LabWidgetManager { this._handleKernelConnectionStatusChange(args); }); - if (context.sessionContext.session?.kernel) { - this._handleKernelChanged({ - name: 'kernel', - oldValue: null, - newValue: context.sessionContext.session?.kernel, - }); - } + this.updateWidgetManager(); + this.setDirty(); this.restoreWidgets(this._context!.model); - this._settings = settings; context.saveState.connect((sender, saveState) => { if (saveState === 'started' && settings.saveState) { this._saveState(); @@ -454,7 +506,7 @@ export class WidgetManager extends LabWidgetManager { * Save the widget state to the context model. */ private _saveState(): void { - const state = this.get_state_sync({ drop_defaults: true }); + const state = this.widgetManager.get_state_sync({ drop_defaults: true }); if (this._context.model.setMetadata) { this._context.model.setMetadata('widgets', { 'application/vnd.jupyter.widget-state+json': state, @@ -468,6 +520,44 @@ export class WidgetManager extends LabWidgetManager { } } + updateWidgetManager() { + if (this._widgetManager) { + this.widgetManager.onUnhandledIOPubMessage.disconnect( + this.onUnhandledIOPubMessage, + this + ); + } + if (this.kernel) { + this._widgetManager = getWidgetManager(this.kernel, this.rendermime); + this._widgetManager.onUnhandledIOPubMessage.connect( + this.onUnhandledIOPubMessage, + this + ); + } + } + + onUnhandledIOPubMessage( + sender: LabWidgetManager, + msg: KernelMessage.IIOPubMessage + ) { + if (WidgetManager.loggerRegistry) { + const logger = WidgetManager.loggerRegistry.getLogger(this.context.path); + let level: LogLevel = 'warning'; + if ( + KernelMessage.isErrorMsg(msg) || + (KernelMessage.isStreamMsg(msg) && msg.content.name === 'stderr') + ) { + level = 'error'; + } + const data: nbformat.IOutput = { + ...msg.content, + output_type: msg.header.msg_type, + }; + // logger.rendermime = this.content.rendermime; + logger.log({ type: 'output', data, level }); + } + } + _handleKernelConnectionStatusChange(status: Kernel.ConnectionStatus): void { if (status === 'connected') { // Only restore if we aren't currently trying to restore from the kernel @@ -483,9 +573,41 @@ export class WidgetManager extends LabWidgetManager { } _handleKernelStatusChange(status: Kernel.Status): void { - if (status === 'restarting') { - this.disconnect(); + this.setDirty(); + } + + get widgetManager(): KernelWidgetManager { + return this._widgetManager; + } + + /** + * A signal emitted when state is restored to the widget manager. + * + * #### Notes + * This indicates that previously-unavailable widget models might be available now. + */ + get restored(): ISignal { + return this._restored; + } + + /** + * Whether the state has been restored yet or not. + */ + get restoredStatus(): boolean { + return this._restoredStatus; + } + + /** + * + * @param renderers + */ + updateWidgetRenderers(renderers: IterableIterator) { + if (this.kernel) { + for (const r of renderers) { + r.manager = this.widgetManager; + } } + // Do we need to handle for if there isn't a kernel? } /** @@ -500,7 +622,6 @@ export class WidgetManager extends LabWidgetManager { if (loadKernel) { try { this._kernelRestoreInProgress = true; - await this._loadFromKernel(); } finally { this._kernelRestoreInProgress = false; } @@ -529,11 +650,21 @@ export class WidgetManager extends LabWidgetManager { // Restore any widgets from saved state that are not live if (widget_md && widget_md[WIDGET_STATE_MIMETYPE]) { let state = widget_md[WIDGET_STATE_MIMETYPE]; - state = this.filterExistingModelState(state); - await this.set_state(state); + state = this.widgetManager.filterModelState(state); + await this.widgetManager.set_state(state); } } + /** + * Get whether the manager is disposed. + * + * #### Notes + * This is a read-only property. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + /** * Dispose the resources held by the manager. */ @@ -543,7 +674,6 @@ export class WidgetManager extends LabWidgetManager { } this._context = null!; - super.dispose(); } /** @@ -562,11 +692,15 @@ export class WidgetManager extends LabWidgetManager { return this._context.sessionContext?.session?.kernel ?? null; } + get rendermime(): IRenderMimeRegistry { + return this._rendermime; + } + /** * Register a widget model. */ register_model(model_id: string, modelPromise: Promise): void { - super.register_model(model_id, modelPromise); + this.widgetManager.register_model(model_id, modelPromise); this.setDirty(); } @@ -575,7 +709,7 @@ export class WidgetManager extends LabWidgetManager { * @return Promise that resolves when the widget state is cleared. */ async clear_state(): Promise { - await super.clear_state(); + // await this.widgetManager.clear_state(); this.setDirty(); } @@ -589,9 +723,15 @@ export class WidgetManager extends LabWidgetManager { this._context!.model.dirty = true; } } - + static loggerRegistry: ILoggerRegistry | null; + protected _restored = new Signal(this); + protected _restoredStatus = false; + private _isDisposed = false; private _context: DocumentRegistry.IContext; + private _rendermime: IRenderMimeRegistry; private _settings: WidgetManager.Settings; + private _widgetManager: KernelWidgetManager; + protected _kernelRestoreInProgress = false; } export namespace WidgetManager { @@ -599,3 +739,49 @@ export namespace WidgetManager { saveState: boolean; }; } + +/** + * Get the widget manager for the kernel. Calling this will ensure + * widgets to work in a kernel. + * With the widgetManager use the method `widgetManager.attachToRendermime` + * against any rendermime. + * @param kernel A kernel connection to which the widget manager is associated. + * @returns LabWidgetManager + */ +export function getWidgetManager( + kernel: Kernel.IKernelConnection, + rendermime: IRenderMimeRegistry +): KernelWidgetManager { + if (!Private.kernelWidgetManagers.has(kernel.id)) { + new KernelWidgetManager(kernel, rendermime); + } + const wManager = Private.kernelWidgetManagers.get(kernel.id); + if (!wManager) { + throw new Error('Failed to create LabWidgetManager'); + } + if (wManager.rendermime !== rendermime) { + wManager.attachToRendermime(rendermime); + } + return wManager; +} + +/** + * Get the widgetManager that owns the model id=model_id. + * @param model_id An existing model_id + * @returns KernelWidgetManager + */ +export function findWidgetManager(model_id: string): KernelWidgetManager { + for (const wManager of Private.kernelWidgetManagers.values()) { + if (wManager.has_model(model_id)) { + return wManager; + } + } + throw new Error(`A widget manager was not found for model_id ${model_id}'`); +} + +/** + * A namespace for private data + */ +namespace Private { + export const kernelWidgetManagers = new ObservableMap(); +} diff --git a/python/jupyterlab_widgets/src/output.ts b/python/jupyterlab_widgets/src/output.ts index 37793bf193..ee559437da 100644 --- a/python/jupyterlab_widgets/src/output.ts +++ b/python/jupyterlab_widgets/src/output.ts @@ -7,13 +7,15 @@ import { JupyterLuminoPanelWidget } from '@jupyter-widgets/base'; import { Panel } from '@lumino/widgets'; -import { LabWidgetManager, WidgetManager } from './manager'; +import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; + +import { LabWidgetManager } from './manager'; import { OutputAreaModel, OutputArea } from '@jupyterlab/outputarea'; import * as nbformat from '@jupyterlab/nbformat'; -import { KernelMessage, Session } from '@jupyterlab/services'; +import { KernelMessage } from '@jupyterlab/services'; import $ from 'jquery'; @@ -33,32 +35,11 @@ export class OutputModel extends outputBase.OutputModel { return false; }; - // if the context is available, react on kernel changes - if (this.widget_manager instanceof WidgetManager) { - this.widget_manager.context.sessionContext.kernelChanged.connect( - (sender, args) => { - this._handleKernelChanged(args); - } - ); - } this.listenTo(this, 'change:msg_id', this.reset_msg_id); this.listenTo(this, 'change:outputs', this.setOutputs); this.setOutputs(); } - /** - * Register a new kernel - */ - _handleKernelChanged({ - oldValue, - }: Session.ISessionConnection.IKernelChangedArgs): void { - const msgId = this.get('msg_id'); - if (msgId && oldValue) { - oldValue.removeMessageHook(msgId, this._msgHook); - this.set('msg_id', null); - } - } - /** * Reset the message id. */ @@ -121,6 +102,7 @@ export class OutputModel extends outputBase.OutputModel { private _msgHook: (msg: KernelMessage.IIOPubMessage) => boolean; private _outputs: OutputAreaModel; + static rendermime: IRenderMimeRegistry; } export class OutputView extends outputBase.OutputView { @@ -145,10 +127,11 @@ export class OutputView extends outputBase.OutputView { render(): void { super.render(); this._outputView = new OutputArea({ - rendermime: this.model.widget_manager.rendermime, + rendermime: OutputModel.rendermime, contentFactory: OutputArea.defaultContentFactory, model: this.model.outputs, }); + // TODO: why is this a readonly property now? // this._outputView.model = this.model.outputs; // TODO: why is this on the model now? diff --git a/python/jupyterlab_widgets/src/plugin.ts b/python/jupyterlab_widgets/src/plugin.ts index f0a16f05b9..68af793c33 100644 --- a/python/jupyterlab_widgets/src/plugin.ts +++ b/python/jupyterlab_widgets/src/plugin.ts @@ -2,7 +2,6 @@ // Distributed under the terms of the Modified BSD License. import { ISettingRegistry } from '@jupyterlab/settingregistry'; -import * as nbformat from '@jupyterlab/nbformat'; import { DocumentRegistry } from '@jupyterlab/docregistry'; @@ -10,19 +9,18 @@ import { INotebookModel, INotebookTracker, Notebook, - NotebookPanel, } from '@jupyterlab/notebook'; import { - JupyterFrontEndPlugin, JupyterFrontEnd, + JupyterFrontEndPlugin, } from '@jupyterlab/application'; import { IMainMenu } from '@jupyterlab/mainmenu'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { ILoggerRegistry, LogLevel } from '@jupyterlab/logconsole'; +import { ILoggerRegistry } from '@jupyterlab/logconsole'; import { CodeCell } from '@jupyterlab/cells'; @@ -34,9 +32,13 @@ import { AttachedProperty } from '@lumino/properties'; import { WidgetRenderer } from './renderer'; -import { WidgetManager, WIDGET_VIEW_MIMETYPE } from './manager'; +import { + LabWidgetManager, + WidgetManager, + WIDGET_VIEW_MIMETYPE, +} from './manager'; -import { OutputModel, OutputView, OUTPUT_WIDGET_VERSION } from './output'; +import { OUTPUT_WIDGET_VERSION, OutputModel, OutputView } from './output'; import * as base from '@jupyter-widgets/base'; @@ -46,11 +48,8 @@ import { JUPYTER_CONTROLS_VERSION } from '@jupyter-widgets/controls/lib/version' import '@jupyter-widgets/base/css/index.css'; import '@jupyter-widgets/controls/css/widgets-base.css'; -import { KernelMessage } from '@jupyterlab/services'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; -const WIDGET_REGISTRY: base.IWidgetRegistryData[] = []; - /** * The cached settings. */ @@ -117,26 +116,11 @@ export function registerWidgetManager( let wManager = Private.widgetManagerProperty.get(context); if (!wManager) { wManager = new WidgetManager(context, rendermime, SETTINGS); - WIDGET_REGISTRY.forEach((data) => wManager!.register(data)); Private.widgetManagerProperty.set(context, wManager); } - - for (const r of renderers) { - r.manager = wManager; + if (wManager.kernel) { + wManager.updateWidgetRenderers(renderers); } - - // Replace the placeholder widget renderer with one bound to this widget - // manager. - rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); - rendermime.addFactory( - { - safe: false, - mimeTypes: [WIDGET_VIEW_MIMETYPE], - createRenderer: (options) => new WidgetRenderer(options, wManager), - }, - -10 - ); - return new DisposableDelegate(() => { if (rendermime) { rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); @@ -183,33 +167,6 @@ function activateWidgetExtension( const { commands } = app; const trans = (translator ?? nullTranslator).load('jupyterlab_widgets'); - const bindUnhandledIOPubMessageSignal = (nb: NotebookPanel): void => { - if (!loggerRegistry) { - return; - } - - const wManager = Private.widgetManagerProperty.get(nb.context); - if (wManager) { - wManager.onUnhandledIOPubMessage.connect( - (sender: WidgetManager, msg: KernelMessage.IIOPubMessage) => { - const logger = loggerRegistry.getLogger(nb.context.path); - let level: LogLevel = 'warning'; - if ( - KernelMessage.isErrorMsg(msg) || - (KernelMessage.isStreamMsg(msg) && msg.content.name === 'stderr') - ) { - level = 'error'; - } - const data: nbformat.IOutput = { - ...msg.content, - output_type: msg.header.msg_type, - }; - logger.rendermime = nb.content.rendermime; - logger.log({ type: 'output', data, level }); - } - ); - } - }; if (settingRegistry !== null) { settingRegistry .load(managerPlugin.id) @@ -221,7 +178,7 @@ function activateWidgetExtension( console.error(reason.message); }); } - + WidgetManager.loggerRegistry = loggerRegistry; // Add a placeholder widget renderer. rendermime.addFactory( { @@ -242,8 +199,6 @@ function activateWidgetExtension( outputViews(app, panel.context.path) ) ); - - bindUnhandledIOPubMessageSignal(panel); }); tracker.widgetAdded.connect((sender, panel) => { registerWidgetManager( @@ -254,8 +209,6 @@ function activateWidgetExtension( outputViews(app, panel.context.path) ) ); - - bindUnhandledIOPubMessageSignal(panel); }); } @@ -284,7 +237,7 @@ function activateWidgetExtension( return { registerWidget(data: base.IWidgetRegistryData): void { - WIDGET_REGISTRY.push(data); + LabWidgetManager.WIDGET_REGISTRY.push(data); }, }; } @@ -356,17 +309,19 @@ export const controlWidgetsPlugin: JupyterFrontEndPlugin = { */ export const outputWidgetPlugin: JupyterFrontEndPlugin = { id: `@jupyter-widgets/jupyterlab-manager:output-${OUTPUT_WIDGET_VERSION}`, - requires: [base.IJupyterWidgetRegistry], + requires: [base.IJupyterWidgetRegistry, IRenderMimeRegistry], autoStart: true, activate: ( app: JupyterFrontEnd, - registry: base.IJupyterWidgetRegistry + registry: base.IJupyterWidgetRegistry, + rendermime: IRenderMimeRegistry ): void => { - registry.registerWidget({ - name: '@jupyter-widgets/output', - version: OUTPUT_WIDGET_VERSION, - exports: { OutputModel, OutputView }, - }); + (OutputModel.rendermime = rendermime), + registry.registerWidget({ + name: '@jupyter-widgets/output', + version: OUTPUT_WIDGET_VERSION, + exports: { OutputModel, OutputView }, + }); }, }; diff --git a/python/jupyterlab_widgets/src/renderer.ts b/python/jupyterlab_widgets/src/renderer.ts index 1e0aa34f37..8b55cce9a2 100644 --- a/python/jupyterlab_widgets/src/renderer.ts +++ b/python/jupyterlab_widgets/src/renderer.ts @@ -5,13 +5,14 @@ import { PromiseDelegate } from '@lumino/coreutils'; import { IDisposable } from '@lumino/disposable'; -import { Panel, Widget as LuminoWidget } from '@lumino/widgets'; +import { Widget as LuminoWidget, Panel } from '@lumino/widgets'; import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; -import { LabWidgetManager } from './manager'; import { DOMWidgetModel } from '@jupyter-widgets/base'; +import { LabWidgetManager, findWidgetManager } from './manager'; + /** * A renderer for widgets. */ @@ -36,14 +37,22 @@ export class WidgetRenderer set manager(value: LabWidgetManager) { value.restored.connect(this._rerender, this); this._manager.resolve(value); + this._manager_set = true; } async renderModel(model: IRenderMime.IMimeModel): Promise { const source: any = model.data[this.mimeType]; - // Let's be optimistic, and hope the widget state will come later. this.node.textContent = 'Loading widget...'; - + if (!this._manager_set) { + try { + this.manager = findWidgetManager(source.model_id); + } catch (err) { + this.node.textContent = `widget model not found for ${model.data['text/plain']}`; + console.error(err); + return Promise.resolve(); + } + } const manager = await this._manager.promise; // If there is no model id, the view was removed, so hide the node. if (source.model_id === '') { @@ -61,12 +70,11 @@ export class WidgetRenderer this.node.textContent = 'Error displaying widget: model not found'; this.addClass('jupyter-widgets'); console.error(err); - return; } // Store the model for a possible rerender this._rerenderMimeModel = model; - return; + return Promise.resolve(); } // Successful getting the model, so we don't need to try to rerender. @@ -121,5 +129,6 @@ export class WidgetRenderer */ readonly mimeType: string; private _manager = new PromiseDelegate(); + private _manager_set = false; private _rerenderMimeModel: IRenderMime.IMimeModel | null = null; } From ae299837c129abea6925b6811fb9820458f947e7 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 25 May 2024 12:27:17 +1000 Subject: [PATCH 38/38] Use first kerenel in KernelWidgetManager. --- python/jupyterlab_widgets/src/manager.ts | 77 ++++++++++++++---------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index c4870099d0..eb7611ab58 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -32,12 +32,7 @@ import * as nbformat from '@jupyterlab/nbformat'; import { ILoggerRegistry, LogLevel } from '@jupyterlab/logconsole'; -import { - Kernel, - KernelConnection, - KernelMessage, - Session, -} from '@jupyterlab/services'; +import { Kernel, KernelMessage, Session } from '@jupyterlab/services'; import { DocumentRegistry } from '@jupyterlab/docregistry'; @@ -365,7 +360,7 @@ export class KernelWidgetManager extends LabWidgetManager { super(rendermime); this.attachToRendermime(rendermime); Private.kernelWidgetManagers.set(kernel.id, this); - this._kernel = new KernelConnection({ model: kernel.model }); + this._kernel = kernel; this.loadCustomWidgetDefinitions(); LabWidgetManager.WIDGET_REGISTRY.changed.connect(() => this.loadCustomWidgetDefinitions() @@ -375,12 +370,11 @@ export class KernelWidgetManager extends LabWidgetManager { this._handleCommOpen ); - this._kernel.statusChanged.connect((sender, args) => { - this._handleKernelStatusChange(args); - }); - this._kernel.connectionStatusChanged.connect((sender, args) => { - this._handleKernelConnectionStatusChange(args); - }); + this._kernel.statusChanged.connect(this._handleKernelStatusChange, this); + this._kernel.connectionStatusChanged.connect( + this._handleKernelConnectionStatusChange, + this + ); this._handleKernelChanged({ name: 'kernel', @@ -390,18 +384,29 @@ export class KernelWidgetManager extends LabWidgetManager { this.restoreWidgets(); } - _handleKernelConnectionStatusChange(status: Kernel.ConnectionStatus): void { - if (status === 'connected') { - // Only restore if we aren't currently trying to restore from the kernel - // (for example, in our initial restore from the constructor). - if (!this._kernelRestoreInProgress) { - this.restoreWidgets(); - } + _handleKernelConnectionStatusChange( + sender: Kernel.IKernelConnection, + status: Kernel.ConnectionStatus + ): void { + switch (status) { + case 'connected': + // Only restore if we aren't currently trying to restore from the kernel + // (for example, in our initial restore from the constructor). + if (!this._kernelRestoreInProgress) { + this.restoreWidgets(); + } + break; + case 'disconnected': + this.dispose(); } } - _handleKernelStatusChange(status: Kernel.Status): void { + _handleKernelStatusChange( + sender: Kernel.IKernelConnection, + status: Kernel.Status + ): void { if (status === 'restarting') { + this.clear_state(); this.disconnect(); } } @@ -478,17 +483,20 @@ export class WidgetManager extends Backbone.Model implements IDisposable { this._context = context; this._settings = settings; - context.sessionContext.kernelChanged.connect((sender, args) => - this.updateWidgetManager() + context.sessionContext.kernelChanged.connect( + this._handleKernelChange, + this ); - context.sessionContext.statusChanged.connect((sender, args) => { - this._handleKernelStatusChange(args); - }); + context.sessionContext.statusChanged.connect( + this._handleStatusChange, + this + ); - context.sessionContext.connectionStatusChanged.connect((sender, args) => { - this._handleKernelConnectionStatusChange(args); - }); + context.sessionContext.connectionStatusChanged.connect( + this._handleConnectionStatusChange, + this + ); this.updateWidgetManager(); this.setDirty(); @@ -558,7 +566,10 @@ export class WidgetManager extends Backbone.Model implements IDisposable { } } - _handleKernelConnectionStatusChange(status: Kernel.ConnectionStatus): void { + _handleConnectionStatusChange( + sender: any, + status: Kernel.ConnectionStatus + ): void { if (status === 'connected') { // Only restore if we aren't currently trying to restore from the kernel // (for example, in our initial restore from the constructor). @@ -572,7 +583,11 @@ export class WidgetManager extends Backbone.Model implements IDisposable { } } - _handleKernelStatusChange(status: Kernel.Status): void { + _handleKernelChange(sender: any, kernel: any): void { + this.updateWidgetManager(); + this.setDirty(); + } + _handleStatusChange(sender: any, status: Kernel.Status): void { this.setDirty(); }