-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Observer pattern #6
Comments
thanks for the pig @davidbrochart! here are some general thoughts that i hope will be helpful: one of the main differences between traitlets and psygnal is that traitlets implements both the observer pattern and the dataclass pattern, while psygnal focuses mostly on carefully implementing the observer pattern, and wants to let the user pick their own modern dataclass pattern, though it does bring some in-library awareness of specific dataclass libs (currently So, the first important question here is: what pattern will you use to declare what the mutable ("dataclass-like") fields are for any given model (i.e. how do we get away from the So, as an example here that omits the If you wanted to use pydantic as your dataclass structure, then you could use from pydantic import PrivateAttr
from psygnal import EventedModel, EmissionInfo
import y_py as Y
class Base(EventedModel):
_ydoc: Y.YDoc = PrivateAttr(default_factory=Y.YDoc)
_ymap: Y.YMap = PrivateAttr()
def __init__(self, **data) -> None:
super().__init__(**data)
self._ymap = self._ydoc.get_map("my_map")
# do initial sync with ydoc
with self._ydoc.begin_transaction() as t:
self._ymap.update(t, self.dict())
# events is a psygnal.SignalGroup, that contains a SignalInstance
# for each field on the model. However, it can *also* be directly connect
# to a callback, which will be called for *any* event on the model
# with the EmissionInfo object passed as the first argument.
self.events.connect(self._on_event)
def _on_event(self, info: EmissionInfo) -> None:
# called anytime any field changes
field_name = info.signal.name
# info.args is a tuple of the arguments "emitted"
# in the most common case of a single argument, we unwrap it here.
new_val = info.args[0] if len(info.args) == 1 else list(info.args)
with self._ydoc.begin_transaction() as t:
self._ymap.set(t, field_name, new_val)
class Switch(Base):
value: bool = False
def toggle(self):
self.value = not self.value In [3]: s = Switch()
In [4]: s._ymap
Out[4]: YMap({'value': False})
In [5]: s.dict()
Out[5]: {'value': False}
In [6]: s.toggle()
In [7]: s._ymap
Out[7]: YMap({'value': True})
In [8]: s.dict()
Out[8]: {'value': True} or, implementing the same thing with from psygnal import evented, SignalGroup, EmissionInfo
from dataclasses import dataclass, asdict
import y_py as Y
class Base:
if TYPE_CHECKING:
# this is what @evented adds
# the typing here is a little funny as a super-class
# but that's another conversation :)
events: SignalGroup
def __post_init__(self):
# same thing that __init__ did above. # this could go in a base class
self._ydoc = Y.YDoc()
self._ymap = self._ydoc.get_map("my_map")
with self._ydoc.begin_transaction() as t:
self._ymap.update(t, asdict(self))
self.events.connect(self._on_event)
def _on_event(self, info: EmissionInfo) -> None:
# same as above
field_name = info.signal.name
new_val = info.args[0] if len(info.args) == 1 else list(info.args)
with self._ydoc.begin_transaction() as t:
self._ymap.set(t, field_name, new_val)
@evented
@dataclass
class Switch(Base):
value: bool = False
def toggle(self):
self.value = not self.value In [3]: s = Switch()
In [4]: s
Out[4]: Switch(value=False)
In [5]: s._ymap
Out[5]: YMap({'value': False})
In [6]: s.toggle()
In [7]: s._ymap
Out[7]: YMap({'value': True})
In [8]: s
Out[8]: Switch(value=True) let me know if that helps a bit, and/or where pain points still exist or arise |
an additional note: the main thing that the
|
That looks awesome! |
yeah, I love pydantic too, and it seems poised to only get better with v2. For this particular application (with a lot of serialization and deserialization), i think |
Hi! Hope you don't mind me chiming in here.
The last time I benchmarked msgspec against pydantic v2, msgspec was between 7 and 20x faster at decoding. Whether that matters is definitely application specific. I'd also expect with the recent 4.5 million they got that they'll eventually narrow that gap, but I'd be surprised if they're ever measurably faster. At this point msgspec is mostly bottlenecked by cpython allocation costs that can't be avoided by any tool.
Please open an issue about any rough edges you find! I definitely want to make msgspec as easy to use as possible. Happy to help! Anyway, please don't feel pressured at all to use msgspec here (or elsewhere). All of these tools have their places and tradeoffs, and whether msgspec's performance improvements matter is definitely application specific. |
hey @jcrist! Don't mind at all, very nice to see you here :) love msgspec!
I would love to discuss the custom init thing a bit. I saw your very reasonable question on jcrist/msgspec#70 asking "what would you use post_init for" ... and my internal answer to myself was so broad (along the lines of "erm ... any custom stuff?") that I didn't think it would add much to the conversation 😂 Generally speaking, I'd like to just be able to do something after initialization of |
Thanks! And that issue was misusing |
@tlambert03 I've been using
Do you know what could go wrong? |
it means that there was an exception in your callback function (but it's not an informative enough message)... easiest approach for now might be to install from github for the moment? |
I opened #7, if you want to take a look. I suspect there is an infinite loop?
OK, I'll try that. |
also ... look higher up in the stack trace. is the cause of the exception printed in the penultimate stack? (with a "The above exception was the direct cause of the following exception:" message) |
I run in an environment where it's not easy to see what's happening, but by printing the traceback when the exception is raised I saw that it's a |
good to know, thanks! I'll get that version out with the better traceback soon |
side-note: by all means let me know if you run into any other pain-points with psygnal or EventedModel specifically |
In jupyter-widgets/ipywidgets#3293 (comment) @tlambert03 mentioned psygnal, that could be used to implement the equivalent of traitlets.
I think it is a good idea, but I'm not sure how it could fit here. For instance, take the Switch model, currently implemented as:
Its
YDoc
consists of aYMap
namedvalue
, with a single entry in it, also namedvalue
, which contains the boolean value of the switch. Here the observer pattern is manually implemented using a getter and a setter function for thevalue
attribute.If we were to use
psygnal
, we would need a way to point to the YDoc's structure (self.yvalue
andvalue
entry in it) from thepsygnal
's attribute.I think that's the reason why
traitlets
use Python's descriptor protocol, which allows to get the name of an attribute at runtime.Also, I'm not sure how we could connect nested
YDoc
structures (e.g. aYMap
can contain otherY
structures), but I don't think it was possible withtraitlets
anyway, so that would be an improvement.The text was updated successfully, but these errors were encountered: