Skip to content

Commit

Permalink
Replace reacttrs with declare (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart authored Apr 19, 2024
1 parent b0fb44d commit 7f5ba7a
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 42 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
`ypywidgets` is a communication backend between a Jupyter kernel and clients. It allows to synchronize data structures that can be modified concurrently, and automatically resolves conflicts. To do so, it uses:
- the Jupyter kernel [Comm](https://jupyter-client.readthedocs.io/en/stable/messaging.html#custom-messages) protocol as the transport layer, and the [comm](https://github.com/ipython/comm) implementation of it.
- the [pycrdt](https://github.com/davidbrochart/pycrdt) CRDT implementation.
- the [reacttrs](https://github.com/davidbrochart/reacttrs) package that implements the observer pattern.
- the [declare](https://github.com/willmcgugan/declare) library that implements the observer pattern and validation.

It is a replacement for (a part of) [ipywidgets](https://ipywidgets.readthedocs.io). When used with [yjs-widgets](https://github.com/davidbrochart/yjs-widgets), it supports JupyterLab clients that implement widgets. The difference with `ipywidgets` is that these widgets are collaborative: they can be manipulated concurrently from the kernel or from any client. The CRDT algorithm ensures that a widget state will eventually be consistent across all clients.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ keywords = [
dependencies = [
"comm >=0.1.4,<1",
"pycrdt >=0.8.2,<0.9.0",
"reacttrs >=0.1.4,<1",
"declare >=1.0.1,<2.0.0",
]

[project.urls]
Expand Down
17 changes: 9 additions & 8 deletions tests/test_attributes.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
from __future__ import annotations
import asyncio
from typing import Optional

import pytest
from pycrdt import Text
from ypywidgets import reactive
from ypywidgets import Declare
from ypywidgets.comm import CommWidget


class Widget1(CommWidget):
foo = reactive("foo1")
bar = reactive("bar1")
baz: reactive[str | None] = reactive(None)
foo = Declare[str]("foo1")
bar = Declare[str]("bar1")
baz = Declare[Optional[str]](None)


class Widget2(CommWidget):
foo = reactive("")
foo = Declare[str]("")

def watch_foo(self, old, new):
@foo.watch
def _watch_foo(self, old, new):
print(f"foo changed: '{old}'->'{new}'")


Expand Down Expand Up @@ -67,4 +68,4 @@ async def test_watch_attribute(widget_factories, synced_widgets, capfd):
# we're seeing the remote widget watch callback
await asyncio.sleep(0.01)
out, err = capfd.readouterr()
assert out == "foo changed: ''->''\nfoo changed: ''->'foo'\n"
assert out == "foo changed: ''->'foo'\n"
2 changes: 1 addition & 1 deletion ypywidgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .widget import Widget as Widget
from .reactive import reactive as reactive
from .declare import Declare as Declare


__version__ = "0.6.5"
48 changes: 48 additions & 0 deletions ypywidgets/declare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from typing import Generic

from pycrdt import ReadTransaction
from declare import Declare as _Declare
from declare._declare import Validator, ValueType, WatchMethodType, WatchDecorator, Watcher

from .widget import Widget


class NestedWatchDecorator(WatchDecorator):
def __call__(
self, method: WatchMethodType | None = None
) -> WatchMethodType | WatchDecorator[WatchMethodType]:
prev_method = self._declare._watcher
if prev_method is None:
_method = method
else:
self._declare._watcher = None
def _method(obj, old, new):
method(obj, old, new)
prev_method(obj, old, new)
super().__call__(_method)


class Declare(_Declare, Generic[ValueType]):

def __init__(
self,
default: ValueType,
*,
validate: Validator | None = None,
watch: Watcher | None = None,
) -> None:
super().__init__(default, validate=validate, watch=watch)

@self.watch
def _set_attr(obj: Widget, old: ValueType, new: ValueType) -> None:
with obj.ydoc.transaction() as txn:
# if we set the attribute, we are observing our own change: do nothing
# we know it's the case because callbacks provide read-only transactions
if not isinstance(txn, ReadTransaction):
obj._attrs[self._name] = new

@property
def watch(self) -> WatchDecorator:
return NestedWatchDecorator(self)
31 changes: 0 additions & 31 deletions ypywidgets/reactive.py

This file was deleted.

0 comments on commit 7f5ba7a

Please sign in to comment.