jupyter-ydoc 3.2.1__py3-none-any.whl → 3.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jupyter_ydoc/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) Jupyter Development Team.
2
2
  # Distributed under the terms of the Modified BSD License.
3
3
 
4
- import sys
4
+ from importlib.metadata import entry_points
5
5
 
6
6
  from ._version import __version__ as __version__
7
7
  from .yblob import YBlob as YBlob
@@ -9,11 +9,4 @@ from .yfile import YFile as YFile
9
9
  from .ynotebook import YNotebook as YNotebook
10
10
  from .yunicode import YUnicode as YUnicode
11
11
 
12
- # See compatibility note on `group` keyword in
13
- # https://docs.python.org/3/library/importlib.metadata.html#entry-points
14
- if sys.version_info < (3, 10):
15
- from importlib_metadata import entry_points
16
- else:
17
- from importlib.metadata import entry_points
18
-
19
12
  ydocs = {ep.name: ep.load() for ep in entry_points(group="jupyter_ydoc")}
jupyter_ydoc/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '3.2.1'
4
+ __version__ = VERSION = '3.3.0'
jupyter_ydoc/utils.py CHANGED
@@ -1,15 +1,12 @@
1
1
  # Copyright (c) Jupyter Development Team.
2
2
  # Distributed under the terms of the Modified BSD License.
3
3
 
4
- from typing import Dict, List, Type, Union
5
4
 
6
- INT = Type[int]
7
- FLOAT = Type[float]
5
+ INT = type[int]
6
+ FLOAT = type[float]
8
7
 
9
8
 
10
- def cast_all(
11
- o: Union[List, Dict], from_type: Union[INT, FLOAT], to_type: Union[FLOAT, INT]
12
- ) -> Union[List, Dict]:
9
+ def cast_all(o: list | dict, from_type: INT | FLOAT, to_type: FLOAT | INT) -> list | dict:
13
10
  if isinstance(o, list):
14
11
  for i, v in enumerate(o):
15
12
  if type(v) is from_type:
jupyter_ydoc/ybasedoc.py CHANGED
@@ -2,7 +2,8 @@
2
2
  # Distributed under the terms of the Modified BSD License.
3
3
 
4
4
  from abc import ABC, abstractmethod
5
- from typing import Any, Callable, Dict, Optional
5
+ from collections.abc import Callable
6
+ from typing import Any
6
7
 
7
8
  from pycrdt import Awareness, Doc, Map, Subscription, UndoManager
8
9
 
@@ -17,10 +18,10 @@ class YBaseDoc(ABC):
17
18
 
18
19
  _ydoc: Doc
19
20
  _ystate: Map
20
- _subscriptions: Dict[Any, Subscription]
21
+ _subscriptions: dict[Any, Subscription]
21
22
  _undo_manager: UndoManager
22
23
 
23
- def __init__(self, ydoc: Optional[Doc] = None, awareness: Optional[Awareness] = None):
24
+ def __init__(self, ydoc: Doc | None = None, awareness: Awareness | None = None):
24
25
  """
25
26
  Constructs a YBaseDoc.
26
27
 
@@ -100,7 +101,7 @@ class YBaseDoc(ABC):
100
101
  return self.set(value)
101
102
 
102
103
  @property
103
- def dirty(self) -> Optional[bool]:
104
+ def dirty(self) -> bool | None:
104
105
  """
105
106
  Returns whether the document is dirty.
106
107
 
@@ -120,7 +121,7 @@ class YBaseDoc(ABC):
120
121
  self._ystate["dirty"] = value
121
122
 
122
123
  @property
123
- def hash(self) -> Optional[str]:
124
+ def hash(self) -> str | None:
124
125
  """
125
126
  Returns the document hash as computed by contents manager.
126
127
 
@@ -140,7 +141,7 @@ class YBaseDoc(ABC):
140
141
  self._ystate["hash"] = value
141
142
 
142
143
  @property
143
- def path(self) -> Optional[str]:
144
+ def path(self) -> str | None:
144
145
  """
145
146
  Returns document's path.
146
147
 
jupyter_ydoc/yblob.py CHANGED
@@ -1,8 +1,9 @@
1
1
  # Copyright (c) Jupyter Development Team.
2
2
  # Distributed under the terms of the Modified BSD License.
3
3
 
4
+ from collections.abc import Callable
4
5
  from functools import partial
5
- from typing import Any, Callable, Optional
6
+ from typing import Any
6
7
 
7
8
  from pycrdt import Awareness, Doc, Map
8
9
 
@@ -24,7 +25,7 @@ class YBlob(YBaseDoc):
24
25
  }
25
26
  """
26
27
 
27
- def __init__(self, ydoc: Optional[Doc] = None, awareness: Optional[Awareness] = None):
28
+ def __init__(self, ydoc: Doc | None = None, awareness: Awareness | None = None):
28
29
  """
29
30
  Constructs a YBlob.
30
31
 
@@ -64,6 +65,9 @@ class YBlob(YBaseDoc):
64
65
  :param value: The content of the document.
65
66
  :type value: bytes
66
67
  """
68
+ if self.get() == value:
69
+ return
70
+
67
71
  self._ysource["bytes"] = value
68
72
 
69
73
  def observe(self, callback: Callable[[str, Any], None]) -> None:
jupyter_ydoc/ynotebook.py CHANGED
@@ -2,8 +2,9 @@
2
2
  # Distributed under the terms of the Modified BSD License.
3
3
 
4
4
  import copy
5
+ from collections.abc import Callable
5
6
  from functools import partial
6
- from typing import Any, Callable, Dict, List, Optional
7
+ from typing import Any
7
8
  from uuid import uuid4
8
9
 
9
10
  from pycrdt import Array, Awareness, Doc, Map, Text
@@ -16,6 +17,8 @@ NBFORMAT_MAJOR_VERSION = 4
16
17
  # The default minor version of the notebook format.
17
18
  NBFORMAT_MINOR_VERSION = 5
18
19
 
20
+ _CELL_KEY_TYPE_MAP = {"metadata": Map, "source": Text, "outputs": Array}
21
+
19
22
 
20
23
  class YNotebook(YBaseDoc):
21
24
  """
@@ -47,7 +50,7 @@ class YNotebook(YBaseDoc):
47
50
  }
48
51
  """
49
52
 
50
- def __init__(self, ydoc: Optional[Doc] = None, awareness: Optional[Awareness] = None):
53
+ def __init__(self, ydoc: Doc | None = None, awareness: Awareness | None = None):
51
54
  """
52
55
  Constructs a YNotebook.
53
56
 
@@ -92,7 +95,7 @@ class YNotebook(YBaseDoc):
92
95
  """
93
96
  return len(self._ycells)
94
97
 
95
- def get_cell(self, index: int) -> Dict[str, Any]:
98
+ def get_cell(self, index: int) -> dict[str, Any]:
96
99
  """
97
100
  Returns a cell.
98
101
 
@@ -104,7 +107,7 @@ class YNotebook(YBaseDoc):
104
107
  """
105
108
  return self._cell_to_py(self._ycells[index])
106
109
 
107
- def _cell_to_py(self, ycell: Map) -> Dict[str, Any]:
110
+ def _cell_to_py(self, ycell: Map) -> dict[str, Any]:
108
111
  meta = self._ymeta.to_py()
109
112
  cell = ycell.to_py()
110
113
  cell.pop("execution_state", None)
@@ -120,7 +123,7 @@ class YNotebook(YBaseDoc):
120
123
  del cell["attachments"]
121
124
  return cell
122
125
 
123
- def append_cell(self, value: Dict[str, Any]) -> None:
126
+ def append_cell(self, value: dict[str, Any]) -> None:
124
127
  """
125
128
  Appends a cell.
126
129
 
@@ -130,7 +133,7 @@ class YNotebook(YBaseDoc):
130
133
  ycell = self.create_ycell(value)
131
134
  self._ycells.append(ycell)
132
135
 
133
- def set_cell(self, index: int, value: Dict[str, Any]) -> None:
136
+ def set_cell(self, index: int, value: dict[str, Any]) -> None:
134
137
  """
135
138
  Sets a cell into indicated position.
136
139
 
@@ -143,7 +146,7 @@ class YNotebook(YBaseDoc):
143
146
  ycell = self.create_ycell(value)
144
147
  self.set_ycell(index, ycell)
145
148
 
146
- def create_ycell(self, value: Dict[str, Any]) -> Map:
149
+ def create_ycell(self, value: dict[str, Any]) -> Map:
147
150
  """
148
151
  Creates YMap with the content of the cell.
149
152
 
@@ -193,7 +196,7 @@ class YNotebook(YBaseDoc):
193
196
  """
194
197
  self._ycells[index] = ycell
195
198
 
196
- def get(self) -> Dict:
199
+ def get(self) -> dict:
197
200
  """
198
201
  Returns the content of the document.
199
202
 
@@ -227,7 +230,7 @@ class YNotebook(YBaseDoc):
227
230
  nbformat_minor=int(meta.get("nbformat_minor", 0)),
228
231
  )
229
232
 
230
- def set(self, value: Dict) -> None:
233
+ def set(self, value: dict) -> None:
231
234
  """
232
235
  Sets the content of the document.
233
236
 
@@ -248,10 +251,10 @@ class YNotebook(YBaseDoc):
248
251
  "id": str(uuid4()),
249
252
  }
250
253
  ]
251
- old_ycells_by_id = {ycell["id"]: ycell for ycell in self._ycells}
254
+ old_ycells_by_id: dict[str, Map] = {ycell["id"]: ycell for ycell in self._ycells}
252
255
 
253
256
  with self._ydoc.transaction():
254
- new_cell_list: List[dict] = []
257
+ new_cell_list: list[dict] = []
255
258
  retained_cells = set()
256
259
 
257
260
  # Determine cells to be retained
@@ -259,7 +262,11 @@ class YNotebook(YBaseDoc):
259
262
  cell_id = new_cell.get("id")
260
263
  if cell_id and (old_ycell := old_ycells_by_id.get(cell_id)):
261
264
  old_cell = self._cell_to_py(old_ycell)
262
- if old_cell == new_cell:
265
+ updated_granularly = self._update_cell(
266
+ old_cell=old_cell, new_cell=new_cell, old_ycell=old_ycell
267
+ )
268
+
269
+ if updated_granularly:
263
270
  new_cell_list.append(old_cell)
264
271
  retained_cells.add(cell_id)
265
272
  continue
@@ -323,3 +330,57 @@ class YNotebook(YBaseDoc):
323
330
  self._subscriptions[self._ystate] = self._ystate.observe(partial(callback, "state"))
324
331
  self._subscriptions[self._ymeta] = self._ymeta.observe_deep(partial(callback, "meta"))
325
332
  self._subscriptions[self._ycells] = self._ycells.observe_deep(partial(callback, "cells"))
333
+
334
+ def _update_cell(self, old_cell: dict, new_cell: dict, old_ycell: Map) -> bool:
335
+ if old_cell == new_cell:
336
+ return True
337
+ # attempt to update cell granularly
338
+ old_keys = set(old_cell.keys())
339
+ new_keys = set(new_cell.keys())
340
+
341
+ shared_keys = old_keys & new_keys
342
+ removed_keys = old_keys - new_keys
343
+ added_keys = new_keys - old_keys
344
+
345
+ for key in shared_keys:
346
+ if old_cell[key] != new_cell[key]:
347
+ value = new_cell[key]
348
+ if key == "output" and value:
349
+ # outputs require complex handling - some have Text type nested;
350
+ # for now skip creating them; clearing all outputs is fine
351
+ return False
352
+
353
+ if key in _CELL_KEY_TYPE_MAP:
354
+ kind = _CELL_KEY_TYPE_MAP[key]
355
+
356
+ if not isinstance(old_ycell[key], kind):
357
+ # if our assumptions about types do not hold, fall back to hard update
358
+ return False
359
+
360
+ if kind == Text:
361
+ old: Text = old_ycell[key]
362
+ old.clear()
363
+ old += value
364
+ elif kind == Array:
365
+ old: Array = old_ycell[key]
366
+ old.clear()
367
+ old.extend(value)
368
+ elif kind == Map:
369
+ old: Map = old_ycell[key]
370
+ old.clear()
371
+ old.update(value)
372
+ else:
373
+ old_ycell[key] = new_cell[key]
374
+
375
+ for key in removed_keys:
376
+ del old_ycell[key]
377
+
378
+ for key in added_keys:
379
+ if key in _CELL_KEY_TYPE_MAP:
380
+ # we hard-reload cells when keys that require nested types get added
381
+ # to allow the frontend to connect observers; this could be changed
382
+ # in the future, once frontends learn how to observe all changes
383
+ return False
384
+ else:
385
+ old_ycell[key] = new_cell[key]
386
+ return True
jupyter_ydoc/yunicode.py CHANGED
@@ -1,8 +1,9 @@
1
1
  # Copyright (c) Jupyter Development Team.
2
2
  # Distributed under the terms of the Modified BSD License.
3
3
 
4
+ from collections.abc import Callable
4
5
  from functools import partial
5
- from typing import Any, Callable, Optional
6
+ from typing import Any
6
7
 
7
8
  from pycrdt import Awareness, Doc, Text
8
9
 
@@ -23,7 +24,7 @@ class YUnicode(YBaseDoc):
23
24
  }
24
25
  """
25
26
 
26
- def __init__(self, ydoc: Optional[Doc] = None, awareness: Optional[Awareness] = None):
27
+ def __init__(self, ydoc: Doc | None = None, awareness: Awareness | None = None):
27
28
  """
28
29
  Constructs a YUnicode.
29
30
 
@@ -67,6 +68,7 @@ class YUnicode(YBaseDoc):
67
68
  # no-op if the values are already the same,
68
69
  # to avoid side-effects such as cursor jumping to the top
69
70
  return
71
+
70
72
  with self._ydoc.transaction():
71
73
  # clear document
72
74
  self._ysource.clear()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jupyter-ydoc
3
- Version: 3.2.1
3
+ Version: 3.3.0
4
4
  Summary: Document structures for collaborative editing using Ypy
5
5
  Project-URL: Homepage, https://jupyter.org
6
6
  Project-URL: Source, https://github.com/jupyter-server/jupyter_ydoc
@@ -8,24 +8,8 @@ Author-email: Jupyter Development Team <jupyter@googlegroups.com>
8
8
  License: BSD 3-Clause License
9
9
  License-File: LICENSE
10
10
  Keywords: jupyter,pycrdt,yjs
11
- Requires-Python: >=3.8
12
- Requires-Dist: importlib-metadata>=3.6; python_version < '3.10'
11
+ Requires-Python: >=3.10
13
12
  Requires-Dist: pycrdt<0.13.0,>=0.10.1
14
- Provides-Extra: dev
15
- Requires-Dist: click; extra == 'dev'
16
- Requires-Dist: jupyter-releaser; extra == 'dev'
17
- Requires-Dist: pre-commit; extra == 'dev'
18
- Provides-Extra: docs
19
- Requires-Dist: myst-parser; extra == 'docs'
20
- Requires-Dist: pydata-sphinx-theme; extra == 'docs'
21
- Requires-Dist: sphinx; extra == 'docs'
22
- Provides-Extra: test
23
- Requires-Dist: httpx-ws>=0.5.2; extra == 'test'
24
- Requires-Dist: hypercorn>=0.16.0; extra == 'test'
25
- Requires-Dist: pre-commit; extra == 'test'
26
- Requires-Dist: pycrdt-websocket<0.17.0,>=0.16.0; extra == 'test'
27
- Requires-Dist: pytest; extra == 'test'
28
- Requires-Dist: pytest-asyncio; extra == 'test'
29
13
  Description-Content-Type: text/markdown
30
14
 
31
15
  [![Build Status](https://github.com/jupyter-server/jupyter_ydoc/workflows/Tests/badge.svg)](https://github.com/jupyter-server/jupyter_ydoc/actions)
@@ -60,8 +44,6 @@ Which is just a shortcut to:
60
44
 
61
45
  ```py
62
46
  from importlib.metadata import entry_points
63
- # for Python < 3.10, install importlib_metadata and do:
64
- # from importlib_metadata import entry_points
65
47
 
66
48
  ydocs = {ep.name: ep.load() for ep in entry_points(group="jupyter_ydoc")}
67
49
  ```
@@ -0,0 +1,14 @@
1
+ jupyter_ydoc/__init__.py,sha256=itUidK7o0_wS6YcbKKIyt1su7hM3-YppQshFheTQQdw,428
2
+ jupyter_ydoc/_version.py,sha256=Tz6lmxmxKUkvsL1d8BVDTboI2Qi1KGyuKbw2_SfyWeo,171
3
+ jupyter_ydoc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ jupyter_ydoc/utils.py,sha256=yKvcuqhpylMinmjuscuZ_kY8KPEseFbwcg5K9VzYOfs,810
5
+ jupyter_ydoc/ybasedoc.py,sha256=c0jwhULtTNCjOYHbXhDhKaD6OJYn7hpL4hcLZWyGJsU,5115
6
+ jupyter_ydoc/yblob.py,sha256=JZiXQhONqFS8Cqdglx__AVeS18gyRq0yHq-AQKFPVfw,2316
7
+ jupyter_ydoc/yfile.py,sha256=XTMtAXDWgIOLU2KUQxkLJz2cGvSPlOxpvJc4daXCV6I,198
8
+ jupyter_ydoc/ynotebook.py,sha256=_nyBC1kKGr1TQlfmFGAMWU1xb0z-vyxRQiLfTukpbAI,13257
9
+ jupyter_ydoc/yunicode.py,sha256=ZLNLTJoy75gxCwI8ZNBv_gD42hrzHfD-J6GOO1WaJIE,2574
10
+ jupyter_ydoc-3.3.0.dist-info/METADATA,sha256=mmVXxTiGc9_QTtRRtjlD2JJpg4Q_JHjaF4tT4JjjQgA,2282
11
+ jupyter_ydoc-3.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ jupyter_ydoc-3.3.0.dist-info/entry_points.txt,sha256=lgvRG-rpsjRKf8cy7LpO7fqwwXy0sBVMCwhGOHgn4mc,164
13
+ jupyter_ydoc-3.3.0.dist-info/licenses/LICENSE,sha256=dqphsFbhnlzPK7Vlkc66Zc7g7PS-e1dln07GXIVpFCQ,1567
14
+ jupyter_ydoc-3.3.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- jupyter_ydoc/__init__.py,sha256=cVoElIvmvfj0IECe1gKe3f5vyyO7Dq7dMB_A0s2zjs4,649
2
- jupyter_ydoc/_version.py,sha256=TB9ypsD-hyVh_lg7CkxRsikKT0D_aW1eqdUCko0bNtw,171
3
- jupyter_ydoc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- jupyter_ydoc/utils.py,sha256=jnjF2uNmioh0z_4ymmMSYiB_2dAKSLmNFzVvR5V5Td0,883
5
- jupyter_ydoc/ybasedoc.py,sha256=RUkrlfu7xp6UfnxhKLB9tHCQyQDTAH10jmQ_AEY5Suo,5119
6
- jupyter_ydoc/yblob.py,sha256=44ZUakq9anNLAcaEuM9RpVN-J6Owf5kFpJJ80mTni8c,2253
7
- jupyter_ydoc/yfile.py,sha256=XTMtAXDWgIOLU2KUQxkLJz2cGvSPlOxpvJc4daXCV6I,198
8
- jupyter_ydoc/ynotebook.py,sha256=y0YHCj9TEwCJOWpA2QepZwp_xQP9i9jq7E5C_y91rvc,10885
9
- jupyter_ydoc/yunicode.py,sha256=s-ZuzSCMVViIWmBxC-gCw9oy3CmE2jT4WG40KW9X3xM,2562
10
- jupyter_ydoc-3.2.1.dist-info/METADATA,sha256=KFpcOvU6mYytpm2Zdu-v1pOOlIDn9cF7wRNYSBkRclQ,3063
11
- jupyter_ydoc-3.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- jupyter_ydoc-3.2.1.dist-info/entry_points.txt,sha256=lgvRG-rpsjRKf8cy7LpO7fqwwXy0sBVMCwhGOHgn4mc,164
13
- jupyter_ydoc-3.2.1.dist-info/licenses/LICENSE,sha256=dqphsFbhnlzPK7Vlkc66Zc7g7PS-e1dln07GXIVpFCQ,1567
14
- jupyter_ydoc-3.2.1.dist-info/RECORD,,