manzt / anywidget

reusable widgets made easy
https://anywidget.dev
MIT License
503 stars 39 forks source link

Restrict ipywidgets dependency version range #63

Closed keller-mark closed 1 year ago

keller-mark commented 1 year ago

Working with a collaborator who is having issues using the vitessce widget. They see the error

Failed to load model class 'AnyModel' from module 'anywidget'
Error: No version of module anywidget is registered

I believe it is related to https://stackoverflow.com/questions/73715821/jupyter-lab-issue-displaying-widgets-javascript-error and https://github.com/jupyterlab/jupyterlab/issues/12977#issuecomment-1221309124

Based on the environment info they shared with me, I am guessing they must have had ipywidgets==8.0.4 installed before trying to install vitessce (of course I can add a dependency on a specific ipywidgets version in the vitessce package, but probably best to fix at the anywidget level)

Minimal reproducer: https://colab.research.google.com/drive/1QkDXWsvyXISdGbR5ETb8HvOqR719lG9s?usp=sharing

manzt commented 1 year ago

Thanks for the issue. I ran into something similar in Google Colab (#48), and tried to implemented a specific fix for that environment #52 to allow for backwards compatibility with ipywidgets v7/8. I will try to reproduce this issue in Jupyter, and then perhaps we can just extend the fix in #52 for all environments.

manzt commented 1 year ago

Ok, I went down a bit of a rabbit hole today with this. However, I have not been able to reproduce the issue in Jupyter/JupyterLab – only Google Colab. Therefore, I'm not sure if this exploration directly addresses this issue, but I have have a deeper understanding of how Colab/Jupyter differ in terms of displaying custom widgets.

TL;DR - the message sent from Python to display the front end requires additional metadata for colab. This metadata is injected by Colab when custom widgets are enabled, but only in the case of display_data messages and not execute_cell messages. We can potentially fix this by injecting the metadata ourselves within _repr_mimebundle_.

At a high level, when you execute a cell, the object at the end of the block is passed to sys.displayhook. So you can reproduce bug with ipywidgets==8.0.4 in Colab with:

import sys

counter = Counter()

sys.displayhook(counter) # 'Widget anywidget AnyModel is not supported' in browser console

in Colab, this triggers a prompt telling the user to enable custom widgets,

image

which does not resolve the issue does not resolve the issue. However, if you manually call IPython.display.display, the widget displays

from IPython.display import display

display(counter) # works if custom widgets are enabled

What gives?

The main difference is that kernel messages sent from sys.displayhook and display are just slightly different. Both functions create a msg dictionary and then call jupyter_client.session.Session.send(<stream>, msg) to send the payload to the front end. You can inspect these messages by enabling logging on the session:

sys.displayhook.session.debug = True
sys.displayhook(counter)
display(counter)
sys.displayhook.session.debug = False

sys.displayhook creates a message of type execute_result whereas display creates a message of type display_data. This alone is not the issue, but moreover that display internally has a "hook" to allow plugins to modify the message contents (which Colab makes use of to enable custom widgets) and sys.displayhook does not. Therefore, messages created with display have the appropriate metadata and execute_results do not (regardless of whether custom widgets are enabled). This can be seen by diffing msg contents:

execute_result, created with sys.displayhook

{'content': {'data': {'application/vnd.jupyter.widget-view+json': {'model_id': 'e475d038722645829dc019e6ce546591',
                                                                   'version_major': 2,
                                                                   'version_minor': 0},
                      'text/plain': 'Counter()'},
             'execution_count': 77,
--           'metadata': {}},
}

display_data, created with display (with widgets disabled)

{'content': {'data': {'application/vnd.jupyter.widget-view+json': {'model_id': 'e475d038722645829dc019e6ce546591',
                                                                   'version_major': 2,
                                                                   'version_minor': 0},
                      'text/plain': 'Counter()'},
--           'metadata': {},
             'transient': {}},

display_data, created with display (with widgets enabled)

{'content': {'data': {'application/vnd.jupyter.widget-view+json': {'model_id': 'e475d038722645829dc019e6ce546591',
                                                                   'version_major': 2,
                                                                   'version_minor': 0},
                      'text/plain': 'Counter()'},
++           'metadata': {'application/vnd.jupyter.widget-view+json': {'colab': {'custom_widget_manager': {'url': 'https://ssl.gstatic.com/colaboratory-static/widgets/colab-cdn-widget-manager/b3e629b1971e1542/manager.min.js'}}}},
             'transient': {}},

Only the last message displays the widget correctly because it's the only case where the colab metadata is sent to the front end. This metadata is injected by a hook included in

`_widget_display_hook` ```python # Copyright 2021 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Support for custom Jupyter Widgets in Colab.""" import IPython as _IPython _supported_widgets_versions = { '5.0.0a': 'b3e629b1971e1542', } _default_version = '5.0.0a' _installed_url = None def enable_custom_widget_manager(version=_default_version): """Enables a Jupyter widget manager which supports custom widgets. This will enable loading the required code from third party websites. Args: version: The version of Jupyter widgets for which support will be enabled. """ version_hash = _supported_widgets_versions.get(version) if not version_hash: raise ValueError( 'Unknown widgets version: {version}'.format(version=version)) _install_custom_widget_manager( '[https://ssl.gstatic.com/colaboratory-static/widgets/colab-cdn-widget-manager/{version_hash}/manager.min.js](https://ssl.gstatic.com/colaboratory-static/widgets/colab-cdn-widget-manager/%7Bversion_hash%7D/manager.min.js)' .format(version_hash=version_hash)) def disable_custom_widget_manager(): """Disable support for custom Jupyter widgets.""" _install_custom_widget_manager(None) def _install_custom_widget_manager(url): """Install a custom Jupyter widget manager. Args: url: The URL to an ES6 module which implements the custom widget manager interface or None to disable third-party widget support. """ global _installed_url if url and not _installed_url: _IPython.get_ipython().display_pub.register_hook(_widget_display_hook) elif not url and _installed_url: _IPython.get_ipython().display_pub.unregister_hook(_widget_display_hook) _installed_url = url _WIDGET_MIME_TYPE = 'application/vnd.jupyter.widget-view+json' def _widget_display_hook(msg): """Display hook to enable custom widget manager info in the display item.""" if not _installed_url: return msg content = msg.get('content', {}) if not content: return msg widget_data = content.get('data', {}).get(_WIDGET_MIME_TYPE) if not widget_data: return msg widget_metadata = content.setdefault('metadata', {}).setdefault(_WIDGET_MIME_TYPE, {}) widget_metadata['colab'] = {'custom_widget_manager': {'url': _installed_url,}} return msg ```

So what to do? In anywidget we could could just inject this metadata ourselves and both execute_result and display_data would render correctly:


    def _repr_mimebundle_(self, **kwargs):
      try:
        import google.colab.output._widgets

        url = google.colab.output._widgets._installed_url
      except ImportError:
        url = None

      meta = {}

      if url:
        meta["application/vnd.jupyter.widget-view+json"] = { 'colab': {'custom_widget_manager': {'url': url} } }

      data = super()._repr_mimebundle_(**kwargs)

      return data, meta
manzt commented 1 year ago

The latest release of anywidget should have more robust rendering for both ipywidget v7 and v8. I'm going to close this for now, but feel free to open again if issues arise.