manzt / anywidget

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

next steps for `MimeBundleDescriptor`: custom messages / non-synchronized state #71

Closed manzt closed 1 year ago

manzt commented 1 year ago

The MimeBundeDescriptor introduces a more library agnostic connection between python model and JS view, but currently doesn't support two important features from ipywidgets.Widget. First a mechanism to both send and receive messages from the front end. The latter could be simple enough to implement with a special method (akin to _anywidget_get_state):

class Foo:
  _repr_mimebundle_ = MimeBundleDescriptor()
  def _anywidget_handle_custom_msg(self, msg):
      ...

But I'm having trouble thinking of a way to allow for custom messages to be sent. Right now this can be accomplished with:

class Foo:
  _repr_mimebundle_ = MimeBundleDescriptor()

Foo()._repr_mimebundle_._comm.send(...) 
# probably do not want to expose Comm directly bc custom messages have a particular format

but I'd be ok with adding a send method to ReprMimeBundle directly (like send_state):

class Foo:
  _repr_mimebundle_ = MimeBundleDescriptor()

Foo()._repr_mimebundle_.send(contents, buffers
# alias for ReprMimeBundle._comm.send({"method": "custom", "content": content}, buffers=buffers)

Of course, using the _repr_mimbundle_ name is a bit verbose, but I like how it makes it clear how these messages are only received if the front end is activated.

Second, we should have a way of marking attributes which shouldn't automatically be synchronized with the frontend. This would be the opposite of sync=True for traitlets where by default everything is synchronized.

tlambert03 commented 1 year ago

I'm not sure I see the _repr_mimebundle_ = MimeBundleDescriptor() as a public facing thing. It could be, but it could also be the building blocks for a thin base class.

For example, you could make AnyWidget itself inherit from HasTraits instead of DOMWidget (or dataclass, or msgspec, etc...), and then the user doesn't particularly care about the details of the descriptor. Something like:

class AnyWidget(t.HasTraits):
    _repr_mimebundle_ = MimeBundleDescriptor()
    def on_msg(self, msg):
        return NotImplemented
    def send(self, contents, buffers):
        ...

also, note that you can always add an alias for _repr_mimebundle_ if you want:

class AnyWidget:
    _repr_mimebundle_ = MimeBundleDescriptor()
    alias = _repr_mimebundle_

_repr_mimebundle_ is, of course, the bare minimum name required to implement the Jupyter API, but you can add more friendly names to access the same object if you want

manzt commented 1 year ago

Might also be worth exploring the comm package instead of relying on ipykernel? https://github.com/jupyter-widgets/ipywidgets/pull/3533 (not sure of the motivation behind this change).

manzt commented 1 year ago

Switched over to comm in #119 , hoping to expand on this effort soon. Would be really neat not require ipywidgets as a dep.