Skip to content

Commit 74d1c15

Browse files
committed
Add YFile and YNotebook
1 parent 414a361 commit 74d1c15

File tree

5 files changed

+267
-0
lines changed

5 files changed

+267
-0
lines changed

.pre-commit-config.yaml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.2.0
4+
hooks:
5+
- id: end-of-file-fixer
6+
- id: check-case-conflict
7+
- id: check-executables-have-shebangs
8+
- id: requirements-txt-fixer
9+
- id: check-added-large-files
10+
- id: check-case-conflict
11+
- id: check-toml
12+
- id: check-yaml
13+
- id: debug-statements
14+
- id: forbid-new-submodules
15+
- id: check-builtin-literals
16+
- id: trailing-whitespace
17+
18+
- repo: https://github.com/psf/black
19+
rev: 22.3.0
20+
hooks:
21+
- id: black
22+
args: ["--line-length", "100"]
23+
24+
- repo: https://github.com/PyCQA/isort
25+
rev: 5.10.1
26+
hooks:
27+
- id: isort
28+
files: \.py$
29+
args: [--profile=black]
30+
31+
- repo: https://github.com/asottile/pyupgrade
32+
rev: v2.32.0
33+
hooks:
34+
- id: pyupgrade
35+
args: [--py37-plus]
36+
37+
- repo: https://github.com/PyCQA/doc8
38+
rev: 0.11.1
39+
hooks:
40+
- id: doc8
41+
args: [--max-line-length=200]
42+
exclude: docs/source/other/full-config.rst
43+
stages: [manual]
44+
45+
- repo: https://github.com/pycqa/flake8
46+
rev: 4.0.1
47+
hooks:
48+
- id: flake8
49+
additional_dependencies:
50+
[
51+
"flake8-bugbear==20.1.4",
52+
"flake8-logging-format==0.6.0",
53+
"flake8-implicit-str-concat==0.2.0",
54+
]
55+
stages: [manual]

jupyter_ydoc/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .ydoc import YFile, YNotebook
2+
3+
__version__ = "0.1.0"

jupyter_ydoc/ydoc.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
from typing import Dict, List, Union
2+
from uuid import uuid4
3+
4+
import y_py as Y
5+
from ypy_websocket.websocket_server import YDoc
6+
7+
8+
class YBaseDoc:
9+
def __init__(self, ydoc: YDoc):
10+
self._ydoc = ydoc
11+
self._ystate = self._ydoc.get_map("state")
12+
self._subscriptions = {}
13+
14+
@property
15+
def ystate(self):
16+
return self._ystate
17+
18+
@property
19+
def ydoc(self):
20+
return self._ydoc
21+
22+
@property
23+
def source(self):
24+
raise RuntimeError("Y document source generation not implemented")
25+
26+
@source.setter
27+
def source(self, value):
28+
raise RuntimeError("Y document source initialization not implemented")
29+
30+
@property
31+
def dirty(self) -> None:
32+
return self._ystate["dirty"]
33+
34+
@dirty.setter
35+
def dirty(self, value: bool) -> None:
36+
if self.dirty != value:
37+
with self._ydoc.begin_transaction() as t:
38+
self._ystate.set(t, "dirty", value)
39+
40+
def observe(self, callback):
41+
raise RuntimeError("Y document observe not implemented")
42+
43+
def unobserve(self):
44+
for k, v in self._subscriptions.items():
45+
k.unobserve(v)
46+
self._subscriptions = {}
47+
48+
49+
class YFile(YBaseDoc):
50+
def __init__(self, *args, **kwargs):
51+
super().__init__(*args, **kwargs)
52+
self._ysource = self._ydoc.get_text("source")
53+
54+
@property
55+
def source(self):
56+
return str(self._ysource)
57+
58+
@source.setter
59+
def source(self, value):
60+
with self._ydoc.begin_transaction() as t:
61+
# clear document
62+
source_len = len(self._ysource)
63+
if source_len:
64+
self._ysource.delete(t, 0, source_len)
65+
# initialize document
66+
if value:
67+
self._ysource.push(t, value)
68+
69+
def observe(self, callback):
70+
self._subscriptions[self._ysource] = self._ysource.observe(callback)
71+
72+
73+
class YNotebook(YBaseDoc):
74+
def __init__(self, *args, **kwargs):
75+
super().__init__(*args, **kwargs)
76+
self._ycells = self._ydoc.get_array("cells")
77+
self._ymeta = self._ydoc.get_map("meta")
78+
79+
@property
80+
def source(self):
81+
cells = self._ycells.to_json()
82+
meta = self._ymeta.to_json()
83+
state = self._ystate.to_json()
84+
cast_all(cells, float, int)
85+
cast_all(meta, float, int)
86+
for cell in cells:
87+
if "id" in cell and state["nbformat"] == 4 and state["nbformatMinor"] <= 4:
88+
# strip cell IDs if we have notebook format 4.0-4.4
89+
del cell["id"]
90+
return dict(
91+
cells=cells,
92+
metadata=meta["metadata"],
93+
nbformat=int(state["nbformat"]),
94+
nbformat_minor=int(state["nbformatMinor"]),
95+
)
96+
97+
@source.setter
98+
def source(self, value):
99+
cast_all(value, int, float)
100+
if not value["cells"]:
101+
value["cells"] = [
102+
{
103+
"cell_type": "code",
104+
"execution_count": None,
105+
"metadata": {},
106+
"outputs": [],
107+
"source": "",
108+
"id": str(uuid4()),
109+
}
110+
]
111+
# workaround until ypy is fixed: https://github.com/davidbrochart/ypy-websocket/pull/9
112+
ytexts_to_clear = []
113+
with self._ydoc.begin_transaction() as t:
114+
# clear document
115+
cells_len = len(self._ycells)
116+
if cells_len:
117+
self._ycells.delete(t, 0, cells_len)
118+
for key in self._ymeta:
119+
self._ymeta.delete(t, key)
120+
for key in [k for k in self._ystate if k != "dirty"]:
121+
self._ystate.delete(t, key)
122+
123+
# initialize document
124+
ycells = []
125+
for cell in value["cells"]:
126+
cell_source = cell["source"]
127+
if cell_source:
128+
ytext = Y.YText(cell_source)
129+
else:
130+
ytext = Y.YText(" ")
131+
ytexts_to_clear.append(ytext)
132+
cell["source"] = ytext
133+
if "outputs" in cell:
134+
cell["outputs"] = Y.YArray(cell["outputs"])
135+
ycell = Y.YMap(cell)
136+
ycells.append(ycell)
137+
138+
if ycells:
139+
self._ycells.push(t, ycells)
140+
self._ymeta.set(t, "metadata", value["metadata"])
141+
self._ystate.set(t, "nbformat", value["nbformat"])
142+
self._ystate.set(t, "nbformatMinor", value["nbformat_minor"])
143+
with self._ydoc.begin_transaction() as t:
144+
for ytext in ytexts_to_clear:
145+
ytext.delete(t, 0, 1)
146+
147+
def observe(self, callback):
148+
self.unobserve()
149+
for cell in self._ycells:
150+
self._subscriptions[cell["source"]] = cell["source"].observe(callback)
151+
if "outputs" in cell:
152+
self._subscriptions[cell["outputs"]] = cell["outputs"].observe(callback)
153+
self._subscriptions[cell] = cell.observe(callback)
154+
self._subscriptions[self._ycells] = self._ycells.observe(callback)
155+
self._subscriptions[self._ymeta] = self._ymeta.observe(callback)
156+
157+
158+
def cast_all(o: Union[List, Dict], from_type, to_type) -> None:
159+
if isinstance(o, list):
160+
for i, v in enumerate(o):
161+
if isinstance(v, from_type):
162+
o[i] = to_type(v)
163+
elif isinstance(v, (list, dict)):
164+
cast_all(v, from_type, to_type)
165+
elif isinstance(o, dict):
166+
for k, v in o.items():
167+
if isinstance(v, from_type):
168+
o[k] = to_type(v)
169+
elif isinstance(v, (list, dict)):
170+
cast_all(v, from_type, to_type)

setup.cfg

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[metadata]
2+
name = jupyter_ydoc
3+
version = attr: jupyter_ydoc.__version__
4+
description = Document structures for collaborative editing using Ypy
5+
long_description = file: README.md
6+
long_description_content_type = text/markdown
7+
license = BSD 3-Clause License
8+
license_files = LICENSE
9+
author = David Brochart
10+
author_email = david.brochart@gmail.com
11+
url = https://github.com/davidbrochart/jupyter_ydoc
12+
platforms = Windows, Linux, Mac OS X
13+
keywords = jupyter ypy
14+
15+
[bdist_wheel]
16+
universal = 1
17+
18+
[options]
19+
include_package_data = True
20+
packages = find:
21+
python_requires = >=3.7
22+
23+
install_requires =
24+
ypy-websocket >=0.1.2
25+
26+
[options.extras_require]
27+
test =
28+
pre-commit
29+
30+
[options.entry_points]
31+
jupyter_ydoc =
32+
file = jupyter_ydoc.ydoc:YFile
33+
notebook = jupyter_ydoc.ydoc:YNotebook
34+
35+
[flake8]
36+
max-line-length = 100

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import setuptools
2+
3+
setuptools.setup()

0 commit comments

Comments
 (0)