Skip to content

Commit

Permalink
Use pycrdt instead of y-py
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart committed Oct 26, 2023
1 parent 8ea8da6 commit 6ece654
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 107 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
run: |
mamba install pip nodejs=18
pip install ".[test]"
pip install https://github.com/davidbrochart/ypy-websocket/archive/pycrdt.zip
- name: Build JavaScript assets
working-directory: javascript
Expand Down
37 changes: 18 additions & 19 deletions jupyter_ydoc/ybasedoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# Distributed under the terms of the Modified BSD License.

from abc import ABC, abstractmethod
from typing import Any, Callable, Optional
from typing import Any, Callable, Dict, Optional

import y_py as Y
from pycrdt import Doc, Map


class YBaseDoc(ABC):
Expand All @@ -15,19 +15,20 @@ class YBaseDoc(ABC):
subscribe to changes in the document.
"""

def __init__(self, ydoc: Optional[Y.YDoc] = None):
def __init__(self, ydoc: Optional[Doc] = None):
"""
Constructs a YBaseDoc.
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document, if provided.
:type ydoc: :class:`y_py.YDoc`, optional.
:param ydoc: The :class:`pycrdt.Doc` that will hold the data of the document, if provided.
:type ydoc: :class:`pycrdt.Doc`, optional.
"""
if ydoc is None:
self._ydoc = Y.YDoc()
self._ydoc = Doc()
else:
self._ydoc = ydoc
self._ystate = self._ydoc.get_map("state")
self._subscriptions = {}
self._ystate = Map()
self._ydoc["state"] = self._ystate
self._subscriptions: Dict[Any, str] = {}

@property
@abstractmethod
Expand All @@ -40,22 +41,22 @@ def version(self) -> str:
"""

@property
def ystate(self) -> Y.YMap:
def ystate(self) -> Map:
"""
A :class:`y_py.YMap` containing the state of the document.
A :class:`pycrdt.Map` containing the state of the document.
:return: The document's state.
:rtype: :class:`y_py.YMap`
:rtype: :class:`pycrdt.Map`
"""
return self._ystate

@property
def ydoc(self) -> Y.YDoc:
def ydoc(self) -> Doc:
"""
The underlying :class:`y_py.YDoc` that contains the data.
The underlying :class:`pycrdt.Doc` that contains the data.
:return: The document's ydoc.
:rtype: :class:`y_py.YDoc`
:rtype: :class:`pycrdt.Doc`
"""
return self._ydoc

Expand Down Expand Up @@ -87,7 +88,7 @@ def dirty(self) -> Optional[bool]:
:return: Whether the document is dirty.
:rtype: Optional[bool]
"""
return self._ystate["dirty"]
return self._ystate.get("dirty")

@dirty.setter
def dirty(self, value: bool) -> None:
Expand All @@ -97,8 +98,7 @@ def dirty(self, value: bool) -> None:
:param value: Whether the document is clean or dirty.
:type value: bool
"""
with self._ydoc.begin_transaction() as t:
self._ystate.set(t, "dirty", value)
self._ystate["dirty"] = value

@property
def path(self) -> Optional[str]:
Expand All @@ -118,8 +118,7 @@ def path(self, value: str) -> None:
:param value: Document's path.
:type value: str
"""
with self._ydoc.begin_transaction() as t:
self._ystate.set(t, "path", value)
self._ystate["path"] = value

@abstractmethod
def get(self) -> Any:
Expand Down
16 changes: 8 additions & 8 deletions jupyter_ydoc/yblob.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from functools import partial
from typing import Any, Callable, Optional, Union

import y_py as Y
from pycrdt import Doc, Map

from .ybasedoc import YBaseDoc

Expand All @@ -28,15 +28,16 @@ class YBlob(YBaseDoc):
}
"""

def __init__(self, ydoc: Optional[Y.YDoc] = None):
def __init__(self, ydoc: Optional[Doc] = None):
"""
Constructs a YBlob.
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document, if provided.
:type ydoc: :class:`y_py.YDoc`, optional.
:param ydoc: The :class:`pycrdt.Doc` that will hold the data of the document, if provided.
:type ydoc: :class:`pycrdt.Doc`, optional.
"""
super().__init__(ydoc)
self._ysource = self._ydoc.get_map("source")
self._ysource = Map()
self._ydoc["source"] = self._ysource

@property
def version(self) -> str:
Expand All @@ -55,7 +56,7 @@ def get(self) -> bytes:
:return: Document's content.
:rtype: bytes
"""
return base64.b64decode(self._ysource.get("base64", "").encode())
return base64.b64decode(self._ysource["base64"].encode())

def set(self, value: Union[bytes, str]) -> None:
"""
Expand All @@ -66,8 +67,7 @@ def set(self, value: Union[bytes, str]) -> None:
"""
if isinstance(value, bytes):
value = base64.b64encode(value).decode()
with self._ydoc.begin_transaction() as t:
self._ysource.set(t, "base64", value)
self._ysource["base64"] = value

def observe(self, callback: Callable[[str, Any], None]) -> None:
"""
Expand Down
95 changes: 35 additions & 60 deletions jupyter_ydoc/ynotebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any, Callable, Dict, Optional
from uuid import uuid4

import y_py as Y
from pycrdt import Array, Doc, Map, Text

from .utils import cast_all
from .ybasedoc import YBaseDoc
Expand Down Expand Up @@ -47,16 +47,18 @@ class YNotebook(YBaseDoc):
}
"""

def __init__(self, ydoc: Optional[Y.YDoc] = None):
def __init__(self, ydoc: Optional[Doc] = None):
"""
Constructs a YNotebook.
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document, if provided.
:type ydoc: :class:`y_py.YDoc`, optional.
:param ydoc: The :class:`pycrdt.Doc` that will hold the data of the document, if provided.
:type ydoc: :class:`pycrdt.Doc`, optional.
"""
super().__init__(ydoc)
self._ymeta = self._ydoc.get_map("meta")
self._ycells = self._ydoc.get_array("cells")
self._ymeta = Map()
self._ycells = Array()
self._ydoc["meta"] = self._ymeta
self._ydoc["cells"] = self._ycells

@property
def version(self) -> str:
Expand All @@ -74,7 +76,7 @@ def ycells(self):
Returns the Y-cells.
:return: The Y-cells.
:rtype: :class:`y_py.YArray`
:rtype: :class:`pycrdt.Array`
"""
return self._ycells

Expand All @@ -98,8 +100,8 @@ def get_cell(self, index: int) -> Dict[str, Any]:
:return: A cell.
:rtype: Dict[str, Any]
"""
meta = json.loads(self._ymeta.to_json())
cell = json.loads(self._ycells[index].to_json())
meta = json.loads(str(self._ymeta))
cell = json.loads(str(self._ycells[index]))
cast_all(cell, float, int) # cells coming from Yjs have e.g. execution_count as float
if "id" in cell and meta["nbformat"] == 4 and meta["nbformat_minor"] <= 4:
# strip cell IDs if we have notebook format 4.0-4.4
Expand All @@ -112,26 +114,17 @@ def get_cell(self, index: int) -> Dict[str, Any]:
del cell["attachments"]
return cell

def append_cell(self, value: Dict[str, Any], txn: Optional[Y.YTransaction] = None) -> None:
def append_cell(self, value: Dict[str, Any]) -> None:
"""
Appends a cell.
:param value: A cell.
:type value: Dict[str, Any]
:param txn: A YTransaction, defaults to None
:type txn: :class:`y_py.YTransaction`, optional.
"""
ycell = self.create_ycell(value)
if txn is None:
with self._ydoc.begin_transaction() as txn:
self._ycells.append(txn, ycell)
else:
self._ycells.append(txn, ycell)

def set_cell(
self, index: int, value: Dict[str, Any], txn: Optional[Y.YTransaction] = None
) -> None:
self._ycells.append(ycell)

def set_cell(self, index: int, value: Dict[str, Any]) -> None:
"""
Sets a cell into indicated position.
Expand All @@ -140,60 +133,48 @@ def set_cell(
:param value: A cell.
:type value: Dict[str, Any]
:param txn: A YTransaction, defaults to None
:type txn: :class:`y_py.YTransaction`, optional.
"""
ycell = self.create_ycell(value)
self.set_ycell(index, ycell, txn)
self.set_ycell(index, ycell)

def create_ycell(self, value: Dict[str, Any]) -> Y.YMap:
def create_ycell(self, value: Dict[str, Any]) -> Map:
"""
Creates YMap with the content of the cell.
:param value: A cell.
:type value: Dict[str, Any]
:return: A new cell.
:rtype: :class:`y_py.YMap`
:rtype: :class:`pycrdt.Map`
"""
cell = copy.deepcopy(value)
if "id" not in cell:
cell["id"] = str(uuid4())
cell_type = cell["cell_type"]
cell_source = cell["source"]
cell_source = "".join(cell_source) if isinstance(cell_source, list) else cell_source
cell["source"] = Y.YText(cell_source)
cell["metadata"] = Y.YMap(cell.get("metadata", {}))
cell["source"] = Text(cell_source)
cell["metadata"] = Map(cell.get("metadata", {}))

if cell_type in ("raw", "markdown"):
if "attachments" in cell and not cell["attachments"]:
del cell["attachments"]
elif cell_type == "code":
cell["outputs"] = Y.YArray(cell.get("outputs", []))
cell["outputs"] = Array(cell.get("outputs", []))

return Y.YMap(cell)
return Map(cell)

def set_ycell(self, index: int, ycell: Y.YMap, txn: Optional[Y.YTransaction] = None) -> None:
def set_ycell(self, index: int, ycell: Map) -> None:
"""
Sets a Y cell into the indicated position.
:param index: The index of the cell.
:type index: int
:param ycell: A YMap with the content of a cell.
:type ycell: :class:`y_py.YMap`
:param txn: A YTransaction, defaults to None
:type txn: :class:`y_py.YTransaction`, optional.
:type ycell: :class:`pycrdt.Map`
"""
if txn is None:
with self._ydoc.begin_transaction() as txn:
self._ycells.delete(txn, index)
self._ycells.insert(txn, index, ycell)
else:
self._ycells.delete(txn, index)
self._ycells.insert(txn, index, ycell)
self._ycells[index] = ycell

def get(self) -> Dict:
"""
Expand All @@ -202,7 +183,7 @@ def get(self) -> Dict:
:return: Document's content.
:rtype: Dict
"""
meta = json.loads(self._ymeta.to_json())
meta = json.loads(str(self._ymeta))
cast_all(meta, float, int) # notebook coming from Yjs has e.g. nbformat as float
cells = []
for i in range(len(self._ycells)):
Expand Down Expand Up @@ -247,29 +228,23 @@ def set(self, value: Dict) -> None:
}
]

with self._ydoc.begin_transaction() as t:
with self._ydoc.transaction():
# clear document
cells_len = len(self._ycells)
for key in self._ymeta:
self._ymeta.pop(t, key)
if cells_len:
self._ycells.delete_range(t, 0, cells_len)
for key in [k for k in self._ystate if k not in ("dirty", "path")]:
self._ystate.pop(t, key)
self._ymeta.clear()
self._ycells.clear()
for key in [k for k in self._ystate.keys() if k not in ("dirty", "path")]:
del self._ystate[key]

# initialize document
# workaround for https://github.com/y-crdt/ypy/issues/126:
# self._ycells.extend(t, [self.create_ycell(cell) for cell in cells])
for cell in cells:
self._ycells.append(t, self.create_ycell(cell))
self._ymeta.set(t, "nbformat", nb.get("nbformat", NBFORMAT_MAJOR_VERSION))
self._ymeta.set(t, "nbformat_minor", nb.get("nbformat_minor", NBFORMAT_MINOR_VERSION))
self._ycells.extend([self.create_ycell(cell) for cell in cells])
self._ymeta["nbformat"] = nb.get("nbformat", NBFORMAT_MAJOR_VERSION)
self._ymeta["nbformat_minor"] = nb.get("nbformat_minor", NBFORMAT_MINOR_VERSION)

metadata = nb.get("metadata", {})
metadata.setdefault("language_info", {"name": ""})
metadata.setdefault("kernelspec", {"name": "", "display_name": ""})

self._ymeta.set(t, "metadata", Y.YMap(metadata))
self._ymeta["metadata"] = Map(metadata)

def observe(self, callback: Callable[[str, Any], None]) -> None:
"""
Expand Down
Loading

0 comments on commit 6ece654

Please sign in to comment.