Skip to content

Commit db30bfa

Browse files
Improve Python API (#122)
* Improve YNotebook Python API * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Call observer's callback with the name of the changed Y object * Update YBaseDoc.observe callback signature * Remove unneeded pass statement * Make external ydoc optional * Add YUTF8 and YBytes * Replace YBytes with YBlob, YUTF8 with YText * Replace YText with YUnicode * Update README Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 0d98b58 commit db30bfa

File tree

3 files changed

+121
-37
lines changed

3 files changed

+121
-37
lines changed

README.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,23 @@
77

88
`jupyter_ydoc` provides [Ypy](https://github.com/y-crdt/ypy)-based data structures for various
99
documents used in the Jupyter ecosystem. Built-in documents include:
10-
- `YFile`: a generic text document.
10+
- `YBlob`: a generic immutable binary document.
11+
- `YUnicode`: a generic UTF8-encoded text document (`YFile` is an alias to `YUnicode`).
1112
- `YNotebook`: a Jupyter notebook document.
1213

13-
These documents are registered via an entry point under the `"jupyter_ydoc"` group as `"file"` and
14-
`"notebook"`, respectively. You can access them as follows:
14+
These documents are registered via an entry point under the `"jupyter_ydoc"` group as `"blob"`,
15+
`"unicode"` (or `"file"`), and `"notebook"`, respectively. You can access them as follows:
1516

1617
```py
1718
from jupyter_ydoc import ydocs
1819

1920
print(ydocs)
20-
# {'file': <class 'jupyter_ydoc.ydoc.YFile'>, 'notebook': <class 'jupyter_ydoc.ydoc.YNotebook'>}
21+
# {
22+
# 'blob': <class 'jupyter_ydoc.ydoc.YBlob'>,
23+
# 'file': <class 'jupyter_ydoc.ydoc.YFile'>,
24+
# 'notebook': <class 'jupyter_ydoc.ydoc.YNotebook'>,
25+
# 'unicode': <class 'jupyter_ydoc.ydoc.YUnicode'>
26+
# }
2127
```
2228

2329
Which is just a shortcut to:
@@ -30,14 +36,13 @@ ydocs = {ep.name: ep.load() for ep in pkg_resources.iter_entry_points(group="jup
3036

3137
Or directly import them:
3238
```py
33-
from jupyter_ydoc import YFile, YNotebook
39+
from jupyter_ydoc import YBlob, YUnicode, YNotebook
3440
```
3541

3642
The `"jupyter_ydoc"` entry point group can be populated with your own documents, e.g. by adding the
37-
following to your package's `setup.cfg`:
43+
following to your package's `pyproject.toml`:
3844

3945
```
40-
[options.entry_points]
41-
jupyter_ydoc =
42-
my_document = my_package.my_file:MyDocumentClass
46+
[project.entry-points.jupyter_ydoc]
47+
my_dodument = "my_package.my_file:MyDocumentClass"
4348
```

jupyter_ydoc/ydoc.py

Lines changed: 105 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Copyright (c) Jupyter Development Team.
22
# Distributed under the terms of the Modified BSD License.
33

4+
import base64
45
import copy
56
from abc import ABC, abstractmethod
6-
from typing import Any, Callable, Dict, Optional
7+
from functools import partial
8+
from typing import Any, Callable, Dict, Optional, Union
79
from uuid import uuid4
810

911
import y_py as Y
@@ -24,14 +26,17 @@ class YBaseDoc(ABC):
2426
subscribe to changes in the document.
2527
"""
2628

27-
def __init__(self, ydoc: Y.YDoc):
29+
def __init__(self, ydoc: Optional[Y.YDoc] = None):
2830
"""
2931
Constructs a YBaseDoc.
3032
31-
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document.
32-
:type ydoc: :class:`y_py.YDoc`
33+
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document, if provided.
34+
:type ydoc: :class:`y_py.YDoc`, optional.
3335
"""
34-
self._ydoc = ydoc
36+
if ydoc is None:
37+
self._ydoc = Y.YDoc()
38+
else:
39+
self._ydoc = ydoc
3540
self._ystate = self._ydoc.get_map("state")
3641
self._subscriptions = {}
3742

@@ -125,7 +130,6 @@ def get(self) -> Any:
125130
:return: Document's content.
126131
:rtype: Any
127132
"""
128-
pass
129133

130134
@abstractmethod
131135
def set(self, value: Any) -> None:
@@ -135,17 +139,15 @@ def set(self, value: Any) -> None:
135139
:param value: The content of the document.
136140
:type value: Any
137141
"""
138-
pass
139142

140143
@abstractmethod
141-
def observe(self, callback: Callable[[Any], None]) -> None:
144+
def observe(self, callback: Callable[[str, Any], None]) -> None:
142145
"""
143146
Subscribes to document changes.
144147
145148
:param callback: Callback that will be called when the document changes.
146-
:type callback: Callable[[Any], None]
149+
:type callback: Callable[[str, Any], None]
147150
"""
148-
pass
149151

150152
def unobserve(self) -> None:
151153
"""
@@ -158,9 +160,9 @@ def unobserve(self) -> None:
158160
self._subscriptions = {}
159161

160162

161-
class YFile(YBaseDoc):
163+
class YUnicode(YBaseDoc):
162164
"""
163-
Extends :class:`YBaseDoc`, and represents a plain text document.
165+
Extends :class:`YBaseDoc`, and represents a plain text document, encoded as UTF-8.
164166
165167
Schema:
166168
@@ -172,12 +174,12 @@ class YFile(YBaseDoc):
172174
}
173175
"""
174176

175-
def __init__(self, ydoc: Y.YDoc):
177+
def __init__(self, ydoc: Optional[Y.YDoc] = None):
176178
"""
177-
Constructs a YFile.
179+
Constructs a YUnicode.
178180
179-
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document.
180-
:type ydoc: :class:`y_py.YDoc`
181+
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document, if provided.
182+
:type ydoc: :class:`y_py.YDoc`, optional.
181183
"""
182184
super().__init__(ydoc)
183185
self._ysource = self._ydoc.get_text("source")
@@ -207,16 +209,81 @@ def set(self, value: str) -> None:
207209
if value:
208210
self._ysource.extend(t, value)
209211

210-
def observe(self, callback: Callable[[Any], None]) -> None:
212+
def observe(self, callback: Callable[[str, Any], None]) -> None:
211213
"""
212214
Subscribes to document changes.
213215
214216
:param callback: Callback that will be called when the document changes.
215-
:type callback: Callable[[Any], None]
217+
:type callback: Callable[[str, Any], None]
216218
"""
217219
self.unobserve()
218-
self._subscriptions[self._ystate] = self._ystate.observe(callback)
219-
self._subscriptions[self._ysource] = self._ysource.observe(callback)
220+
self._subscriptions[self._ystate] = self._ystate.observe(partial(callback, "state"))
221+
self._subscriptions[self._ysource] = self._ysource.observe(partial(callback, "source"))
222+
223+
224+
class YFile(YUnicode): # for backwards-compatibility
225+
pass
226+
227+
228+
class YBlob(YBaseDoc):
229+
"""
230+
Extends :class:`YBaseDoc`, and represents a blob document.
231+
It is currently encoded as base64 because of:
232+
https://github.com/y-crdt/ypy/issues/108#issuecomment-1377055465
233+
The Y document can be set from bytes or from str, in which case it is assumed to be encoded as
234+
base64.
235+
236+
Schema:
237+
238+
.. code-block:: json
239+
240+
{
241+
"state": YMap,
242+
"source": YMap
243+
}
244+
"""
245+
246+
def __init__(self, ydoc: Optional[Y.YDoc] = None):
247+
"""
248+
Constructs a YBlob.
249+
250+
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document, if provided.
251+
:type ydoc: :class:`y_py.YDoc`, optional.
252+
"""
253+
super().__init__(ydoc)
254+
self._ysource = self._ydoc.get_map("source")
255+
256+
def get(self) -> bytes:
257+
"""
258+
Returns the content of the document.
259+
260+
:return: Document's content.
261+
:rtype: bytes
262+
"""
263+
return base64.b64decode(self._ysource.get("base64", "").encode())
264+
265+
def set(self, value: Union[bytes, str]) -> None:
266+
"""
267+
Sets the content of the document.
268+
269+
:param value: The content of the document.
270+
:type value: Union[bytes, str]
271+
"""
272+
if isinstance(value, bytes):
273+
value = base64.b64encode(value).decode()
274+
with self._ydoc.begin_transaction() as t:
275+
self._ysource.set(t, "base64", value)
276+
277+
def observe(self, callback: Callable[[str, Any], None]) -> None:
278+
"""
279+
Subscribes to document changes.
280+
281+
:param callback: Callback that will be called when the document changes.
282+
:type callback: Callable[[str, Any], None]
283+
"""
284+
self.unobserve()
285+
self._subscriptions[self._ystate] = self._ystate.observe(partial(callback, "state"))
286+
self._subscriptions[self._ysource] = self._ysource.observe(partial(callback, "source"))
220287

221288

222289
class YNotebook(YBaseDoc):
@@ -248,17 +315,27 @@ class YNotebook(YBaseDoc):
248315
}
249316
"""
250317

251-
def __init__(self, ydoc: Y.YDoc):
318+
def __init__(self, ydoc: Optional[Y.YDoc] = None):
252319
"""
253320
Constructs a YNotebook.
254321
255-
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document.
256-
:type ydoc: :class:`y_py.YDoc`
322+
:param ydoc: The :class:`y_py.YDoc` that will hold the data of the document, if provided.
323+
:type ydoc: :class:`y_py.YDoc`, optional.
257324
"""
258325
super().__init__(ydoc)
259326
self._ymeta = self._ydoc.get_map("meta")
260327
self._ycells = self._ydoc.get_array("cells")
261328

329+
@property
330+
def cell_number(self) -> int:
331+
"""
332+
Returns the number of cells in the notebook.
333+
334+
:return: The cell number.
335+
:rtype: int
336+
"""
337+
return len(self._ycells)
338+
262339
def get_cell(self, index: int) -> Dict[str, Any]:
263340
"""
264341
Returns a cell.
@@ -438,14 +515,14 @@ def set(self, value: Dict) -> None:
438515

439516
self._ymeta.set(t, "metadata", Y.YMap(metadata))
440517

441-
def observe(self, callback: Callable[[Any], None]) -> None:
518+
def observe(self, callback: Callable[[str, Any], None]) -> None:
442519
"""
443520
Subscribes to document changes.
444521
445522
:param callback: Callback that will be called when the document changes.
446-
:type callback: Callable[[Any], None]
523+
:type callback: Callable[[str, Any], None]
447524
"""
448525
self.unobserve()
449-
self._subscriptions[self._ystate] = self._ystate.observe(callback)
450-
self._subscriptions[self._ymeta] = self._ymeta.observe_deep(callback)
451-
self._subscriptions[self._ycells] = self._ycells.observe_deep(callback)
526+
self._subscriptions[self._ystate] = self._ystate.observe(partial(callback, "state"))
527+
self._subscriptions[self._ymeta] = self._ymeta.observe_deep(partial(callback, "meta"))
528+
self._subscriptions[self._ycells] = self._ycells.observe_deep(partial(callback, "cells"))

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ docs = [
4040
]
4141

4242
[project.entry-points.jupyter_ydoc]
43+
blob = "jupyter_ydoc.ydoc:YBlob"
4344
file = "jupyter_ydoc.ydoc:YFile"
45+
unicode = "jupyter_ydoc.ydoc:YUnicode"
4446
notebook = "jupyter_ydoc.ydoc:YNotebook"
4547

4648
[project.readme]

0 commit comments

Comments
 (0)