`display` shows nothing while directly outputing the widget is OK
Describe the bug
Hi thanks for the lib! However, it seems display shows nothing.
This is OK:
But this shows nothing:
With logs being normal (no errors)
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
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.
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())
Hi thanks you for the quick reply! But importing the display function does not work for me:
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.
Yes it's working!
More observations:
- Variable in function also not work
- Variable referenced by global variable does work
- Variable in function, but referenced by global var, does work
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.
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
Thanks, then look like this can be fixed and looking forward to the future release!
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)