-
Notifications
You must be signed in to change notification settings - Fork 38
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
feat: Patch ipywidgets to be more flexible with child widgets #629
base: main
Are you sure you want to change the base?
Conversation
|
For example, with this PR, widget authors could extend directly from import traitlets
import ipywidgets
from anywidget._descriptor import MimeBundleDescriptor
esm = """
export function render({ model, el }) {
let count = () => model.get("value");
let btn = document.createElement("button");
btn.classList.add("counter-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);
}
"""
css = """
.counter-button {
background-color: rgba(148, 163, 184, 0.1);
border-radius: 10px;
padding: 0.5em 1em;
font-size: 2em;
}
"""
class Counter(traitlets.HasTraits):
_repr_mimebundle_ = MimeBundleDescriptor(_esm=esm, _css=css)
value = traitlets.Int(0).tag(sync=True)
counter = Counter()
ipywidgets.VBox([counter, ipywidgets.IntSlider()]) |
@tlambert03, I'm really flummoxed here. I believe it's crucial to maintain compatibility with these top-level APIs from ipywidgets, but the deep connection with ipywidgets.Widget is really holding us back. I think this is the minimal "patching" that is necessary, but it's definitely a hack and I don't love that it mutates ipywidgets. I just worry it's going to take a long time to upstream something like this. |
haven't fully looked through your (he asks, almost as if to suggest such a thing is possible...) |
Yes... that would solve everything I think (if we also exposed a |
hmmm, yeah, i was hoping i could be sneaky with |
here's another crazy black magic idea: In [8]: import ipywidgets
In [9]: class MyThing:
...: @property
...: def __class__(self):
...: return ipywidgets.Widget
...:
In [10]: isinstance(MyThing(), ipywidgets.Widget)
Out[10]: True so, taking that one step further, if you did a little stack inspection inside of that |
import inspect
from ipywidgets.widgets.widget import _widget_to_json
import ipywidgets
class LieToIpyWidgets:
def __get__(self, obj, objtype=None):
if obj is None:
return self
stack = inspect.stack()
for frame in stack:
if "ipywidgets" in frame.filename: # this could be better of course
return ipywidgets.Widget
return objtype
class Thing:
__class__ = LieToIpyWidgets()
model_id = "1234"
thing = Thing()
print(isinstance(thing, ipywidgets.Widget))
print(_widget_to_json(thing, {}))
(you could even |
not saying it's much better than what you're currently doing ... but I do definitely hesitate to monkey patch other peoples libraries, and they would be justified in being annoyed by that |
Totally. I like your solution a lot better. I'm going to go in that direction. Thanks so much for the experimentation and thoughts. As always, they are very creative and have the right amount of magic. I always learn something :) I'm not sure I would have come to a similar alternative. |
Summary
ipywidgets performs an
isinstance(x, Widget)
check with their container classes (i.e.,Box
,Link
). This restricts the use of only classes that derive fromipywidgets.Widget
, which is a very large class with many methods.This is problematic because it ties the entire widgets ecosystem (and anywidget) to a large, monolithic class. Widget authors inherit from this class, and now their top-level APIs for end users have a set of methods that should be considered private to end users but are useful for widget developers.
This makes it difficult to innovate on Python-side APIs, as developers are forced to inherit from this large class even if they only need basic widget functionality.
In anywidget, it really holds us back from iterating on the Python side of thing because we don't want to break compatability with ipywidgets but we need other APIs (#579, #628).
Solution
This PR patches ipywidgets to allow widgets composed from non-
Widget
base classes. The patch performs flexible serialization and deserialization by checking for the model_id attribute instead of strict instance checks.We should probably come up with a protocol for getting the
model_id
attribute, making this even less tied to anywidget/ipywidgets, but for now, this is a good start.For example, you can now implement a very simple widget with our descriptor API: