jupyter-server-ydoc 2.2.0b0__tar.gz → 2.2.1__tar.gz

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.
Files changed (29) hide show
  1. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/PKG-INFO +1 -1
  2. jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/_version.py +1 -0
  3. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/loaders.py +1 -1
  4. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/pytest_plugin.py +12 -2
  5. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/rooms.py +16 -6
  6. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/stores.py +8 -1
  7. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/test_utils.py +7 -1
  8. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/tests/test_app.py +17 -1
  9. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/tests/test_documents.py +2 -1
  10. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/tests/test_handlers.py +66 -0
  11. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/tests/test_loaders.py +22 -0
  12. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/tests/test_rooms.py +103 -0
  13. jupyter_server_ydoc-2.2.0b0/jupyter_server_ydoc/_version.py +0 -1
  14. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/.gitignore +0 -0
  15. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/LICENSE +0 -0
  16. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/README.md +0 -0
  17. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter-config/jupyter_server_ydoc.json +0 -0
  18. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/__init__.py +0 -0
  19. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/app.py +0 -0
  20. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/events/awareness.yaml +0 -0
  21. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/events/fork.yaml +0 -0
  22. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/events/session.yaml +0 -0
  23. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/handlers.py +0 -0
  24. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/utils.py +0 -0
  25. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/jupyter_server_ydoc/websocketserver.py +0 -0
  26. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/pyproject.toml +0 -0
  27. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/setup.py +0 -0
  28. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/tests/__init__.py +0 -0
  29. {jupyter_server_ydoc-2.2.0b0 → jupyter_server_ydoc-2.2.1}/tests/conftest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jupyter-server-ydoc
3
- Version: 2.2.0b0
3
+ Version: 2.2.1
4
4
  Summary: jupyter-server extension integrating collaborative shared models.
5
5
  Project-URL: Documentation, https://jupyterlab-realtime-collaboration.readthedocs.io/
6
6
  Project-URL: Repository, https://github.com/jupyterlab/jupyter-collaboration
@@ -0,0 +1 @@
1
+ __version__ = "2.2.1"
@@ -86,7 +86,7 @@ class FileLoader:
86
86
  try:
87
87
  await self._watcher
88
88
  except asyncio.CancelledError:
89
- self._log.info(f"file watcher for '{self.file_id}' is cancelled now")
89
+ self._log.info(f"File watcher for '{self.file_id}' was cancelled")
90
90
 
91
91
  def observe(
92
92
  self,
@@ -30,7 +30,14 @@ def rtc_document_save_delay():
30
30
 
31
31
 
32
32
  @pytest.fixture
33
- def jp_server_config(jp_root_dir, jp_server_config, rtc_document_save_delay):
33
+ def rtc_document_cleanup_delay():
34
+ return 60
35
+
36
+
37
+ @pytest.fixture
38
+ def jp_server_config(
39
+ jp_root_dir, jp_server_config, rtc_document_save_delay, rtc_document_cleanup_delay
40
+ ):
34
41
  return {
35
42
  "ServerApp": {
36
43
  "jpserver_extensions": {
@@ -47,7 +54,10 @@ def jp_server_config(jp_root_dir, jp_server_config, rtc_document_save_delay):
47
54
  "db_path": str(jp_root_dir.joinpath(".fid_test.db")),
48
55
  "db_journal_mode": "OFF",
49
56
  },
50
- "YDocExtension": {"document_save_delay": rtc_document_save_delay},
57
+ "YDocExtension": {
58
+ "document_save_delay": rtc_document_save_delay,
59
+ "document_cleanup_delay": rtc_document_cleanup_delay,
60
+ },
51
61
  }
52
62
 
53
63
 
@@ -278,20 +278,28 @@ class DocumentRoom(YRoom):
278
278
  return
279
279
 
280
280
  self._saving_document = asyncio.create_task(
281
- self._maybe_save_document(self._saving_document)
281
+ self._maybe_save_document(self._saving_document, save_now=True)
282
282
  )
283
283
  return self._saving_document
284
284
 
285
- async def _maybe_save_document(self, saving_document: asyncio.Task | None) -> None:
285
+ async def _maybe_save_document(
286
+ self, saving_document: asyncio.Task | None, save_now: bool = False
287
+ ) -> None:
286
288
  """
287
289
  Saves the content of the document to disk.
288
290
 
289
291
  ### Note:
290
292
  There is a save delay to debounce the save since we could receive a high
291
293
  amount of changes in a short period of time. This way we can cancel the
292
- previous save.
294
+ previous save. When save_now is True, the delay is skipped and the save
295
+ executes immediately.
296
+
297
+ Parameters:
298
+ saving_document: The previous saving task to cancel if needed.
299
+ save_now: If True, skip the debounce delay, and save immediately.
300
+ This is used when manually saving.
293
301
  """
294
- if self._save_delay is None:
302
+ if self._save_delay is None and not save_now:
295
303
  return
296
304
  if saving_document is not None and not saving_document.done():
297
305
  # the document is being saved, cancel that
@@ -301,8 +309,10 @@ class DocumentRoom(YRoom):
301
309
  # because this coroutine is run in a cancellable task and cancellation is handled here
302
310
 
303
311
  try:
304
- # save after X seconds of inactivity
305
- await asyncio.sleep(self._save_delay)
312
+ # When save_now is False, wait X seconds of inactivity before saving (auto-save).
313
+ # When save_now is True, save immediately without debounce delay (manual save).
314
+ if not save_now and self._save_delay is not None:
315
+ await asyncio.sleep(self._save_delay)
306
316
 
307
317
  self.log.info("Saving the content from room %s", self._room_id)
308
318
  saved_model = await self._file.maybe_save_content(
@@ -27,10 +27,17 @@ class SQLiteYStore(LoggingConfigurable, _SQLiteYStore, metaclass=SQLiteYStoreMet
27
27
  directory.""",
28
28
  )
29
29
 
30
- document_ttl = Int(
30
+ squash_after_inactivity_of = Int(
31
31
  None,
32
32
  allow_none=True,
33
33
  config=True,
34
34
  help="""The document time-to-live in seconds. Defaults to None (document history is never
35
35
  cleared).""",
36
36
  )
37
+ document_ttl = Int(
38
+ None,
39
+ allow_none=True,
40
+ config=True,
41
+ help="""The document time-to-live in seconds. Deprecated in favor of 'squash_after_inactivity_of'.
42
+ Defaults to None (document history is never cleared).""",
43
+ )
@@ -33,13 +33,19 @@ class FakeContentsManager:
33
33
  "mimetype": None,
34
34
  "size": 0,
35
35
  "writable": False,
36
+ "hash": "fake_hash",
36
37
  }
37
38
  self.model.update(model)
38
39
 
39
40
  self.actions: list[str] = []
40
41
 
41
42
  def get(
42
- self, path: str, content: bool = True, format: str | None = None, type: str | None = None
43
+ self,
44
+ path: str,
45
+ content: bool = True,
46
+ format: str | None = None,
47
+ type: str | None = None,
48
+ require_hash: bool | None = None,
43
49
  ) -> dict:
44
50
  if not self.model:
45
51
  raise HTTPError(404, f"File not found: {path}")
@@ -74,7 +74,23 @@ async def test_document_ttl_from_settings(rtc_create_mock_document_room, jp_conf
74
74
  rtc_create_SQLite_store = rtc_create_SQLite_store_factory(app)
75
75
  store = await rtc_create_SQLite_store("file", id, content)
76
76
 
77
- assert store.document_ttl == 3600
77
+ # document_ttl is deprecated and mapped to squash_after_inactivity_of
78
+ assert store.squash_after_inactivity_of == 3600
79
+
80
+
81
+ async def test_squash_after_inactivity_of_from_settings(
82
+ rtc_create_mock_document_room, jp_configurable_serverapp
83
+ ):
84
+ argv = ["--SQLiteYStore.squash_after_inactivity_of=3600"]
85
+
86
+ app = jp_configurable_serverapp(argv=argv)
87
+
88
+ id = "test-id"
89
+ content = "test_ttl"
90
+ rtc_create_SQLite_store = rtc_create_SQLite_store_factory(app)
91
+ store = await rtc_create_SQLite_store("file", id, content)
92
+
93
+ assert store.squash_after_inactivity_of == 3600
78
94
 
79
95
 
80
96
  @pytest.mark.parametrize("copy", [True, False])
@@ -69,7 +69,8 @@ async def test_room_concurrent_initialization(
69
69
  tg.start_soon(connect, file_format, file_type, file_path)
70
70
  tg.start_soon(connect, file_format, file_type, file_path)
71
71
  t1 = time()
72
- assert t1 - t0 < 0.5
72
+ delta = t1 - t0
73
+ assert delta < 0.6
73
74
 
74
75
  await cleanup(jp_serverapp)
75
76
 
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import json
7
+ import pytest
7
8
  from asyncio import Event, sleep
8
9
  from typing import Any
9
10
 
@@ -133,6 +134,71 @@ async def test_room_handler_doc_client_should_emit_awareness_event(
133
134
  assert collected_data[1]["username"] is not None
134
135
 
135
136
 
137
+ @pytest.fixture
138
+ def rtc_document_cleanup_delay():
139
+ return 2
140
+
141
+
142
+ async def test_room_handler_doc_client_should_stop_file_watcher(
143
+ rtc_create_file, rtc_connect_doc_client, jp_serverapp
144
+ ):
145
+ path, _ = await rtc_create_file("test.txt", "test")
146
+ fim = jp_serverapp.web_app.settings["file_id_manager"]
147
+ file_loaders = jp_serverapp.web_app.settings["jupyter_server_ydoc"].file_loaders
148
+
149
+ event = Event()
150
+
151
+ def _on_document_change(target: str, e: Any) -> None:
152
+ if target == "source":
153
+ event.set()
154
+
155
+ doc = YUnicode()
156
+ doc.observe(_on_document_change)
157
+
158
+ websocket, room_name = await rtc_connect_doc_client("text", "file", path)
159
+ async with websocket as ws, Provider(doc.ydoc, HttpxWebsocket(ws, room_name)):
160
+ await event.wait()
161
+ file_id = fim.get_id("test.txt")
162
+ assert file_id in file_loaders
163
+ file_loader = file_loaders[file_id]
164
+ await sleep(0.1)
165
+
166
+ listener_was_called = False
167
+ collected_data = []
168
+
169
+ async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None:
170
+ nonlocal listener_was_called
171
+ collected_data.append(data)
172
+ listener_was_called = True
173
+
174
+ event_logger = jp_serverapp.event_logger
175
+ event_logger.add_listener(
176
+ schema_id="https://schema.jupyter.org/jupyter_collaboration/session/v1",
177
+ listener=my_listener,
178
+ )
179
+
180
+ file_watcher = file_loader._watcher
181
+
182
+ # Before cleanup delay, the file watcher should still be running
183
+ assert not file_watcher.done()
184
+
185
+ # Wait for the cleanup delay (2 seconds) plus a buffer (0.5 seconds)
186
+ await sleep(2.5)
187
+
188
+ assert listener_was_called is True
189
+ assert len(collected_data) == 2
190
+ assert collected_data[0]["msg"] == "Room deleted."
191
+ assert collected_data[0]["path"] == "test.txt"
192
+ assert collected_data[1]["msg"] == "Loader deleted."
193
+ assert collected_data[1]["path"] == "test.txt"
194
+
195
+ # After the cleanup delay, the file watcher should be done
196
+ assert file_watcher.done()
197
+
198
+ await jp_serverapp.web_app.settings["jupyter_server_ydoc"].stop_extension()
199
+ del jp_serverapp.web_app.settings["file_id_manager"]
200
+
201
+
136
202
  async def test_room_handler_doc_client_should_cleanup_room_file(
137
203
  rtc_create_file, rtc_connect_doc_client, jp_serverapp
138
204
  ):
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import asyncio
7
+ import logging
7
8
  from datetime import datetime, timedelta, timezone
8
9
 
9
10
  from jupyter_server_ydoc.loaders import FileLoader, FileLoaderMapping
@@ -82,6 +83,27 @@ async def test_FileLoader_with_watcher_errors(caplog):
82
83
  await loader.clean()
83
84
 
84
85
 
86
+ async def test_FileLoader_clean_logs_cancellation(caplog):
87
+ id = "file-4567"
88
+ path = "myfile.txt"
89
+ paths = {id: path}
90
+
91
+ cm = FakeContentsManager({"last_modified": datetime.now(timezone.utc)})
92
+ loader = FileLoader(
93
+ id,
94
+ FakeFileIDManager(paths),
95
+ cm,
96
+ poll_interval=0.05,
97
+ )
98
+ await loader.load_content("text", "file")
99
+
100
+ caplog.set_level(logging.INFO)
101
+ await loader.clean()
102
+
103
+ messages = [r.getMessage() for r in caplog.records]
104
+ assert f"File watcher for '{id}' was cancelled" in messages
105
+
106
+
85
107
  async def test_FileLoader_without_watcher():
86
108
  id = "file-4567"
87
109
  path = "myfile.txt"
@@ -121,6 +121,109 @@ async def test_should_save_content_when_at_least_one_client_has_autosave_enabled
121
121
  assert "save" in cm.actions
122
122
 
123
123
 
124
+ async def test_manual_save_should_not_have_delay(
125
+ rtc_create_mock_document_room,
126
+ ):
127
+ content = "test"
128
+ cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=0.5)
129
+
130
+ await room.initialize()
131
+
132
+ # Trigger a manual save
133
+ room._save_to_disc()
134
+
135
+ # Manual save should execute immediately, without waiting for the 0.5s delay
136
+ # Check that save happens within a very short time (100ms should be enough)
137
+ await asyncio.sleep(0.1)
138
+
139
+ assert cm.actions.count("save") == 1
140
+
141
+
142
+ async def test_manual_save_with_pending_autosave_should_cancel_autosave(
143
+ rtc_create_mock_document_room,
144
+ ):
145
+ content = "test"
146
+ cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=1.0)
147
+
148
+ await room.initialize()
149
+
150
+ room._document.source = "Test 2"
151
+
152
+ await asyncio.sleep(0.1)
153
+
154
+ assert cm.actions.count("save") == 0
155
+
156
+ save_task = room._save_to_disc()
157
+
158
+ # Manual save should execute immediately
159
+ await asyncio.sleep(0.1)
160
+ assert save_task.done()
161
+
162
+ # Check that the manual save was recorded
163
+ assert cm.actions.count("save") == 1
164
+
165
+ await asyncio.sleep(1.0)
166
+
167
+ # There should be only one save (the manual one), not two
168
+ assert cm.actions.count("save") == 1
169
+
170
+
171
+ async def test_manual_save_should_execute_immediately_even_with_long_delay(
172
+ rtc_create_mock_document_room,
173
+ ):
174
+ content = "test"
175
+ cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=5.0)
176
+
177
+ await room.initialize()
178
+
179
+ save_task = room._save_to_disc()
180
+
181
+ await asyncio.sleep(0.5)
182
+
183
+ assert "save" in cm.actions
184
+ assert save_task.done()
185
+
186
+
187
+ async def test_autosave_should_still_have_delay(
188
+ rtc_create_mock_document_room,
189
+ ):
190
+ content = "test"
191
+ save_delay = 0.3
192
+ cm, _, room = rtc_create_mock_document_room(
193
+ "test-id", "test.txt", content, save_delay=save_delay
194
+ )
195
+
196
+ await room.initialize()
197
+
198
+ room._document.source = "Test 3"
199
+
200
+ await asyncio.sleep(0.1)
201
+ assert "save" not in cm.actions
202
+
203
+ # Wait for the delay to complete
204
+ await asyncio.sleep(save_delay)
205
+
206
+ assert "save" in cm.actions
207
+
208
+
209
+ async def test_manual_save_should_work_when_save_delay_is_none_and_save_now_is_true(
210
+ rtc_create_mock_document_room,
211
+ ):
212
+ """Test that manual saves execute even when save_delay is None."""
213
+ content = "test"
214
+ # When save_delay is None, autosave is disabled
215
+ cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=None)
216
+
217
+ await room.initialize()
218
+
219
+ # Trigger a manual save with save_now=True
220
+ # Even though save_delay is None, manual saves should still work
221
+ await room._maybe_save_document(None, save_now=True)
222
+
223
+ # Manual save should have executed
224
+ assert cm.actions.count("save") == 1
225
+
226
+
124
227
  # The following test should be restored when package versions are fixed.
125
228
 
126
229
  # async def test_document_path(rtc_create_mock_document_room):
@@ -1 +0,0 @@
1
- __version__ = "2.2.0b0"