diff --git a/jupyter_ydoc/__init__.py b/jupyter_ydoc/__init__.py index 8bca86c..5d9e55e 100644 --- a/jupyter_ydoc/__init__.py +++ b/jupyter_ydoc/__init__.py @@ -3,11 +3,11 @@ import sys -from ._version import __version__ # noqa -from .yblob import YBlob # noqa -from .yfile import YFile # noqa -from .ynotebook import YNotebook # noqa -from .yunicode import YUnicode # noqa +from ._version import __version__ +from .yblob import YBlob +from .yfile import YFile +from .ynotebook import YNotebook +from .yunicode import YUnicode # See compatibility note on `group` keyword in # https://docs.python.org/3/library/importlib.metadata.html#entry-points diff --git a/jupyter_ydoc/ybasedoc.py b/jupyter_ydoc/ybasedoc.py index 44f9799..3d9aa81 100644 --- a/jupyter_ydoc/ybasedoc.py +++ b/jupyter_ydoc/ybasedoc.py @@ -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): @@ -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 @@ -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 @@ -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: @@ -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]: @@ -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: diff --git a/jupyter_ydoc/yblob.py b/jupyter_ydoc/yblob.py index 3950361..9cfdcc0 100644 --- a/jupyter_ydoc/yblob.py +++ b/jupyter_ydoc/yblob.py @@ -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 @@ -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: @@ -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: """ @@ -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: """ diff --git a/jupyter_ydoc/ynotebook.py b/jupyter_ydoc/ynotebook.py index 47ae466..3d32133 100644 --- a/jupyter_ydoc/ynotebook.py +++ b/jupyter_ydoc/ynotebook.py @@ -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 @@ -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: @@ -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 @@ -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 @@ -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. @@ -140,14 +133,11 @@ 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. @@ -155,7 +145,7 @@ def create_ycell(self, value: Dict[str, Any]) -> Y.YMap: :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: @@ -163,18 +153,18 @@ def create_ycell(self, value: Dict[str, Any]) -> Y.YMap: 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. @@ -182,18 +172,9 @@ def set_ycell(self, index: int, ycell: Y.YMap, txn: Optional[Y.YTransaction] = N :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: """ @@ -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)): @@ -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: """ diff --git a/jupyter_ydoc/yunicode.py b/jupyter_ydoc/yunicode.py index 9e416bb..0dda6ea 100644 --- a/jupyter_ydoc/yunicode.py +++ b/jupyter_ydoc/yunicode.py @@ -4,7 +4,7 @@ from functools import partial from typing import Any, Callable, Optional -import y_py as Y +from pycrdt import Doc, Text from .ybasedoc import YBaseDoc @@ -23,15 +23,16 @@ class YUnicode(YBaseDoc): } """ - def __init__(self, ydoc: Optional[Y.YDoc] = None): + def __init__(self, ydoc: Optional[Doc] = None): """ Constructs a YUnicode. - :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_text("source") + self._ysource = Text() + self._ydoc["source"] = self._ysource @property def version(self) -> str: @@ -59,14 +60,12 @@ def set(self, value: str) -> None: :param value: The content of the document. :type value: str """ - with self._ydoc.begin_transaction() as t: + with self._ydoc.transaction(): # clear document - source_len = len(self._ysource) - if source_len: - self._ysource.delete_range(t, 0, source_len) + del self._ysource[:] # initialize document if value: - self._ysource.extend(t, value) + self._ysource += value def observe(self, callback: Callable[[str, Any], None]) -> None: """ diff --git a/pyproject.toml b/pyproject.toml index 7923d5c..4aee802 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,8 @@ description = "Document structures for collaborative editing using Ypy" requires-python = ">=3.7" keywords = ["jupyter", "ypy"] dependencies = [ - "importlib_metadata >=3.6; python_version<\"3.10\"", - "y-py >=0.6.0,<0.7.0", + "importlib_metadata >=3.6; python_version<'3.10'", + "pycrdt >=0.3.1,<0.4.0", ] [[project.authors]] diff --git a/tests/test_ypy_yjs.py b/tests/test_ypy_yjs.py index 5ba157b..ea1804f 100644 --- a/tests/test_ypy_yjs.py +++ b/tests/test_ypy_yjs.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest -import y_py as Y +from pycrdt import Doc from websockets import connect # type: ignore from ypy_websocket import WebsocketProvider @@ -27,7 +27,7 @@ def stringify_source(nb: dict) -> dict: class YTest: - def __init__(self, ydoc: Y.YDoc, timeout: float = 1.0): + def __init__(self, ydoc: Doc, timeout: float = 1.0): self.timeout = timeout self.ytest = ydoc.get_map("_test") with ydoc.begin_transaction() as t: @@ -51,7 +51,7 @@ def source(self): @pytest.mark.asyncio @pytest.mark.parametrize("yjs_client", "0", indirect=True) async def test_ypy_yjs_0(yws_server, yjs_client): - ydoc = Y.YDoc() + ydoc = Doc() ynotebook = YNotebook(ydoc) websocket = await connect("ws://localhost:1234/my-roomname") WebsocketProvider(ydoc, websocket) @@ -64,7 +64,7 @@ async def test_ypy_yjs_0(yws_server, yjs_client): def test_plotly_renderer(): """This test checks in particular that the type cast is not breaking the data.""" - ydoc = Y.YDoc() + ydoc = Doc() ynotebook = YNotebook(ydoc) nb = stringify_source(json.loads((files_dir / "plotly_renderer.ipynb").read_text())) ynotebook.source = nb