Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix: make .widget and .widget_types deprecated #3567

Merged
4 changes: 4 additions & 0 deletions docs/source/user_migration_guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ The previously deprecated traits `overflow_x` and `overflow_y`
[have been removed](https://github.com/jupyter-widgets/ipywidgets/pull/2688). Please
use the `overflow` trait instead.

#### `Widget.widgets` and `Widget.widget_types` attributes

The `Widget` class attributes `.widgets` and `.widget_types` are now deprecated and relocated to internal module-level private variables, opening up these attribute names on the `Widget` class for future uses.

### Deployments

#### Embedded CDN
Expand Down
6 changes: 3 additions & 3 deletions python/ipywidgets/ipywidgets/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import json
import re
from .widgets import Widget, DOMWidget
from .widgets import Widget, DOMWidget, widget as widget_module
from .widgets.widget_link import Link
from .widgets.docutils import doc_subst
from ._version import __html_manager_version__
Expand Down Expand Up @@ -129,7 +129,7 @@ def _get_recursive_state(widget, store=None, drop_defaults=False):

def add_resolved_links(store, drop_defaults):
"""Adds the state of any link models between two models in store"""
for widget_id, widget in Widget._active_widgets.items(): # go over all widgets
for widget_id, widget in widget_module._instances.items(): # go over all widgets
if isinstance(widget, Link) and widget_id not in store:
if widget.source[0].model_id in store and widget.target[0].model_id in store:
store[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults)
Expand Down Expand Up @@ -207,7 +207,7 @@ def embed_data(views, drop_defaults=True, state=None):
view_specs: a list of widget view specs
"""
if views is None:
views = [w for w in Widget._active_widgets.values() if isinstance(w, DOMWidget)]
views = [w for w in widget_module._instances.values() if isinstance(w, DOMWidget)]
else:
try:
views[0]
Expand Down
4 changes: 2 additions & 2 deletions python/ipywidgets/ipywidgets/tests/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import traitlets

from ..widgets import IntSlider, IntText, Text, Widget, jslink, HBox, widget_serialization
from ..widgets import IntSlider, IntText, Text, Widget, jslink, HBox, widget_serialization, widget as widget_module
from ..embed import embed_data, embed_snippet, embed_minimal_html, dependency_state


Expand All @@ -29,7 +29,7 @@ class CaseWidget(Widget):
class TestEmbed:

def teardown(self):
for w in tuple(Widget._active_widgets.values()):
for w in tuple(widget_module._instances.values()):
w.close()

def test_embed_data_simple(self):
Expand Down
18 changes: 16 additions & 2 deletions python/ipywidgets/ipywidgets/widgets/tests/test_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from IPython.display import display
from IPython.utils.capture import capture_output

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

Expand Down Expand Up @@ -49,9 +50,22 @@ def test_close_all():
# create a couple of widgets
widgets = [Button() for i in range(10)]

assert len(Widget._active_widgets) > 0, "expect active widgets"
assert len(widget._instances) > 0, "expect active widgets"

# close all the widgets
Widget.close_all()

assert len(Widget._active_widgets) == 0, "active widgets should be cleared"
assert len(widget._instances) == 0, "active widgets should be cleared"


def test_compatibility():
button = Button()
assert button in widget.Widget.widgets.values()
assert widget._instances is widget.Widget.widgets
assert widget._instances is widget.Widget._active_widgets
Widget.close_all()
assert not widget.Widget.widgets
assert not widget.Widget._active_widgets

assert widget.Widget.widget_types is widget._registry
assert widget.Widget._widget_types is widget._registry
61 changes: 48 additions & 13 deletions python/ipywidgets/ipywidgets/widgets/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
in the Jupyter notebook front-end.
"""
import os
import typing
from contextlib import contextmanager
from collections.abc import Iterable
import warnings
from IPython import get_ipython
from ipykernel.comm import Comm
from traitlets import (
Expand All @@ -34,6 +36,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:
# https://github.com/jupyter-widgets/ipywidgets/issues/1345
_instances : typing.MutableMapping[str, "Widget"] = {}

def _widget_to_json(x, obj):
if isinstance(x, dict):
Expand All @@ -50,8 +55,8 @@ def _json_to_widget(x, obj):
return {k: _json_to_widget(v, obj) for k, v in x.items()}
elif isinstance(x, (list, tuple)):
return [_json_to_widget(v, obj) for v in x]
elif isinstance(x, str) and x.startswith('IPY_MODEL_') and x[10:] in Widget._active_widgets:
return Widget._active_widgets[x[10:]]
elif isinstance(x, str) and x.startswith('IPY_MODEL_') and x[10:] in _instances:
return _instances[x[10:]]
else:
return x

Expand Down Expand Up @@ -259,10 +264,16 @@ def items(self):
for view_name, widget in sorted(vn.items()):
yield (model_module, model_version, model_name, view_module, view_version, view_name), widget



# a registry of widgets by module, version, and name so we can create a Python model from widgets
# that are constructed from the frontend.
_registry = WidgetRegistry()

def register(widget):
"""A decorator registering a widget class in the widget registry."""
w = widget.class_traits()
Widget._widget_types.register(w['_model_module'].default_value,
_registry.register(w['_model_module'].default_value,
w['_model_module_version'].default_value,
w['_model_name'].default_value,
w['_view_module'].default_value,
Expand All @@ -272,22 +283,46 @@ def register(widget):
return widget


class _staticproperty(object):
def __init__(self, fget):
self.fget = fget

def __get__(self, owner_self, owner_cls):
assert owner_self is None
return self.fget()



class Widget(LoggingHasTraits):
#-------------------------------------------------------------------------
# Class attributes
#-------------------------------------------------------------------------
_widget_construction_callback = None
_control_comm = None

# _active_widgets is a dictionary of all active widget objects
_active_widgets = {}
@_staticproperty
def widgets():
warnings.warn("Widget.widgets is deprecated.", DeprecationWarning)
return _instances

@_staticproperty
def _active_widgets():
warnings.warn("Widget._active_widgets is deprecated.", DeprecationWarning)
return _instances

@_staticproperty
def _widget_types():
warnings.warn("Widget._widget_types is deprecated.", DeprecationWarning)
return _registry

# _widget_types is a registry of widgets by module, version, and name:
_widget_types = WidgetRegistry()
@_staticproperty
def widget_types():
warnings.warn("Widget.widget_types is deprecated.", DeprecationWarning)
return _registry

@classmethod
def close_all(cls):
for widget in list(cls._active_widgets.values()):
for widget in list(_instances.values()):
widget.close()

@staticmethod
Expand Down Expand Up @@ -329,7 +364,7 @@ def _handle_control_comm_msg(cls, msg):
if method == 'request_states':
# Send back the full widgets state
cls.get_manager_state()
widgets = cls._active_widgets.values()
widgets = _instances.values()
full_state = {}
drop_defaults = False
for widget in widgets:
Expand Down Expand Up @@ -359,7 +394,7 @@ def handle_comm_opened(comm, msg):
state = data['state']

# Find the widget class to instantiate in the registered widgets
widget_class = Widget._widget_types.get(state['_model_module'],
widget_class = register.get(state['_model_module'],
state['_model_module_version'],
state['_model_name'],
state['_view_module'],
Expand All @@ -380,7 +415,7 @@ def get_manager_state(drop_defaults=False, widgets=None):
"""
state = {}
if widgets is None:
widgets = Widget._active_widgets.values()
widgets = _instances.values()
for widget in widgets:
state[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults)
return {'version_major': 2, 'version_minor': 0, 'state': state}
Expand Down Expand Up @@ -476,7 +511,7 @@ def _comm_changed(self, change):
self._model_id = self.model_id

self.comm.on_msg(self._handle_msg)
Widget._active_widgets[self.model_id] = self
_instances[self.model_id] = self

@property
def model_id(self):
Expand All @@ -496,7 +531,7 @@ def close(self):
When the comm is closed, all of the widget views are automatically
removed from the front-end."""
if self.comm is not None:
Widget._active_widgets.pop(self.model_id, None)
_instances.pop(self.model_id, None)
self.comm.close()
self.comm = None
self._repr_mimebundle_ = None
Expand Down