anywidget icon indicating copy to clipboard operation
anywidget copied to clipboard

`display` shows nothing while directly outputing the widget is OK

Open fzyzcjy opened this issue 1 year ago • 9 comments

Describe the bug

Hi thanks for the lib! However, it seems display shows nothing.

This is OK:

image

But this shows nothing:

image

With logs being normal (no errors)

image

Reproduction

from pydantic import BaseModel
import anywidget.experimental
import psygnal

_counter_esm = """
function render({ model, el }) {
  let button = document.createElement("button");
  button.innerHTML = `count is ${model.get("value")}`;
  button.addEventListener("click", () => {
    model.set("value", model.get("value") + 1);
    model.save_changes();
  });
  model.on("change:value", () => {
    button.innerHTML = `count is ${model.get("value")}`;
  });
  el.classList.add("counter-widget");
  el.appendChild(button);
}
export default { render };
"""

@anywidget.experimental.widget(esm=_counter_esm)
@psygnal.evented
class HelloWidget(BaseModel):
  value: int = 0

# HelloWidget(value=42)
display(HelloWidget(value=42))

Logs

No response

System Info

jupyter lab

Severity

annoyance

fzyzcjy avatar Dec 12 '24 01:12 fzyzcjy

Hi there, I am able to reproduce. I'm not sure this is something we can resolve in the experimental API.

I'm not sure why, but it seems like the global display might special case ipywidgets and the experimental API in anywidget is without ipywidgets.

I was able to get this to work by importing from IPython.display explicitly:

++ from IPython.display import display

from pydantic import BaseModel
import anywidget.experimental
import psygnal

@anywidget.experimental.widget(esm="""
function render({ model, el }) {
  let button = document.createElement("button");
  button.innerHTML = `count is ${model.get("value")}`;
  button.addEventListener("click", () => {
    model.set("value", model.get("value") + 1);
    model.save_changes();
  });
  model.on("change:value", () => {
    button.innerHTML = `count is ${model.get("value")}`;
  });
  el.classList.add("counter-widget");
  el.appendChild(button);
}
export default { render };
""")
@psygnal.evented
class HelloWidget(BaseModel):
  value: int = 0

display(HelloWidget(value=42))

For context, the display method calls HelloWidget()._repr_mimebundle_() internally, and the returned value is identical between the experimental and ipywidgets-based APIs. So I'm not entirely sure how the special casing is happening, or if there is a way we could emulate it as well for the class without traitlets/ipywidgets.

manzt avatar Dec 12 '24 01:12 manzt

For anyone else reading this, display works for the core ipywidgets-based API:

import anywidget
import traitlets

class CounterWidget(anywidget.AnyWidget):
    _esm = """
    function render({ model, el }) {
      let count = () => model.get("value");
      let btn = document.createElement("button");
      btn.innerHTML = `count is ${count()}`;
      btn.addEventListener("click", () => {
        model.set("value", count() + 1);
        model.save_changes();
      });
      model.on("change:value", () => {
        btn.innerHTML = `count is ${count()}`;
      });
      el.appendChild(btn);
    }
    export default { render };
    """
    value = traitlets.Int(0).tag(sync=True)

display(CounterWidget())

manzt avatar Dec 12 '24 01:12 manzt

Hi thanks you for the quick reply! But importing the display function does not work for me:

image

fzyzcjy avatar Dec 12 '24 02:12 fzyzcjy

Ah you know what, I think I figured something out. I don't think it is related to the display import, my mistake.

display(HelloWidget(value=42))

Does not show anything for me. But assigning the widget to variable first:

w = HelloWidget(value=42)
display(w)

does work for me. Would you be able to try both and confirm? I can have a look to understand the underlying behavior later if this is the root of the issue.

manzt avatar Dec 12 '24 02:12 manzt

Yes it's working!

image

fzyzcjy avatar Dec 12 '24 12:12 fzyzcjy

More observations:

  1. Variable in function also not work

image

  1. Variable referenced by global variable does work

image

  1. Variable in function, but referenced by global var, does work

image

fzyzcjy avatar Dec 12 '24 12:12 fzyzcjy

I think I find something: The object is weirdly deleted (if we do not reference it in a global variable)! Surely, if it is deleted, then everything will go wrong.

image image

fzyzcjy avatar Dec 12 '24 12:12 fzyzcjy

Thanks for the exploration! ok, now i’m realizing that this is due to fact we make widgets with our Descriptor weakrefable. The comms are getting cleaned up.

cc: @tlambert03

manzt avatar Dec 12 '24 12:12 manzt

Thanks, then look like this can be fixed and looking forward to the future release!

fzyzcjy avatar Dec 12 '24 12:12 fzyzcjy

Just wanted to follow up from a recent discussion, I think this behavior will end up being documented as expected rather than "fixed".

Our goal with the descriptor API is to create a more flexible and maintainable foundation than ipywidgets. We are not aiming for full compatibility. Instead, we are trying to improve on ipywidgets by avoiding its large API surface area and hidden behaviors / memory issues.

In this case, the behavior is a direct result of using weakrefs to avoid leaking widgets (which ipywidgets has). We do not currently see a clean way to support both strong internal references and automatic cleanup. So for now, the tradeoff is that end user must hold a strong reference if they want the widget to persist when using display.

This is a slight usability downside, but we believe the explicitness is a better long-term default. If you want to use display, you can always write a small helper:

import IPython.display

_widget_refs = {}

def display(obj):
    _widget_refs[id(obj)] = obj
    return IPython.display.display(obj)

manzt avatar Aug 06 '25 15:08 manzt