Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-jupyter-server-ydoc for
openSUSE:Factory checked in at 2026-03-23 17:12:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jupyter-server-ydoc (Old)
and /work/SRC/openSUSE:Factory/.python-jupyter-server-ydoc.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-jupyter-server-ydoc"
Mon Mar 23 17:12:32 2026 rev:5 rq:1341876 version:2.2.1
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-jupyter-server-ydoc/python-jupyter-server-ydoc.changes
2025-07-03 12:13:03.504183522 +0200
+++
/work/SRC/openSUSE:Factory/.python-jupyter-server-ydoc.new.8177/python-jupyter-server-ydoc.changes
2026-03-23 17:13:16.633447919 +0100
@@ -1,0 +2,27 @@
+Sun Mar 22 17:10:38 UTC 2026 - Ben Greiner <[email protected]>
+
+- Update to 2.2.1
+ * New subpackage version for jupyter-collaboration 4.2.1
+
+-------------------------------------------------------------------
+Sun Mar 15 19:16:31 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2.1.5:
+ * Backport 'Initialize and update the ydoc path property' #342
+ #357 (@brichet)
+ * \[2.x\] Support @jupyter/ydoc 2.x #316 (@fcollonval)
+- update to 2.1.4:
+ * Backport 'Update jupyter_ydoc and pycrdt_websocket
+ dependencies' #367 #376 (@brichet)
+- update to 2.1.3:
+ * Backport 'Fix model format' #368 #369 (@davidbrochart)
+ * Backport 'Fix ignoring AnyIO warnings in tests' #359 #362
+ * Backport 'Fix mypy' #358 #361 (@brichet)
+
+-------------------------------------------------------------------
+Mon Dec 1 21:34:54 UTC 2025 - Dirk Müller <[email protected]>
+
+- update to 2.1.2:
+ * New subpackage version for jupyter-collaboration 4.1.2
+
+-------------------------------------------------------------------
Old:
----
jupyter_server_ydoc-2.1.0.tar.gz
New:
----
jupyter_server_ydoc-2.2.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-jupyter-server-ydoc.spec ++++++
--- /var/tmp/diff_new_pack.e0A6g5/_old 2026-03-23 17:13:17.769494959 +0100
+++ /var/tmp/diff_new_pack.e0A6g5/_new 2026-03-23 17:13:17.773495125 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-jupyter-server-ydoc
#
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -25,9 +25,9 @@
%bcond_with test
%endif
-%define distversion 2.1
+%define distversion 2.2.1
Name: python-jupyter-server-ydoc%{psuffix}
-Version: 2.1.0
+Version: 2.2.1
Release: 0
Summary: Jupyter server extension integrating collaborative shared
models
License: BSD-3-Clause
@@ -44,8 +44,8 @@
Requires: python-pycrdt
Requires: (python-jupyter_server >= 2.15.0 with python-jupyter_server <
3.0)
Requires: (python-jupyter_server_fileid >= 0.7.0 with
python-jupyter_server_fileid < 1)
-Requires: (python-jupyter_ydoc >= 2.1.2 with python-jupyter_ydoc < 4)
-Requires: (python-pycrdt-websocket >= 0.15.0 with
python-pycrdt-websocket < 0.16)
+Requires: (python-jupyter_ydoc >= 3.0.3 with python-jupyter_ydoc < 4)
+Requires: (python-pycrdt-websocket >= 0.16.0 with
python-pycrdt-websocket < 0.17)
Provides: python-jupyter_server_ydoc = %{version}-%{release}
BuildArch: noarch
%if %{with test}
++++++ jupyter_server_ydoc-2.1.0.tar.gz -> jupyter_server_ydoc-2.2.1.tar.gz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server_ydoc-2.1.0/PKG-INFO
new/jupyter_server_ydoc-2.2.1/PKG-INFO
--- old/jupyter_server_ydoc-2.1.0/PKG-INFO 2020-02-02 01:00:00.000000000
+0100
+++ new/jupyter_server_ydoc-2.2.1/PKG-INFO 2020-02-02 01:00:00.000000000
+0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: jupyter-server-ydoc
-Version: 2.1.0
+Version: 2.2.1
Summary: jupyter-server extension integrating collaborative shared models.
Project-URL: Documentation,
https://jupyterlab-realtime-collaboration.readthedocs.io/
Project-URL: Repository, https://github.com/jupyterlab/jupyter-collaboration
@@ -84,9 +84,9 @@
Requires-Dist: jupyter-events>=0.11.0
Requires-Dist: jupyter-server-fileid<1,>=0.7.0
Requires-Dist: jupyter-server<3.0.0,>=2.15.0
-Requires-Dist: jupyter-ydoc!=3.0.0,!=3.0.1,<4.0.0,>=2.1.2
+Requires-Dist: jupyter-ydoc<4.0.0,>=3.0.3
Requires-Dist: pycrdt
-Requires-Dist: pycrdt-websocket<0.16.0,>=0.15.0
+Requires-Dist: pycrdt-websocket<0.17.0,>=0.16.0
Provides-Extra: test
Requires-Dist: anyio; extra == 'test'
Requires-Dist: coverage; extra == 'test'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/_version.py
new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/_version.py
--- old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/_version.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/_version.py
2020-02-02 01:00:00.000000000 +0100
@@ -1 +1 @@
-__version__ = "2.1.0"
+__version__ = "2.2.1"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/app.py
new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/app.py
--- old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/app.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/app.py 2020-02-02
01:00:00.000000000 +0100
@@ -10,7 +10,7 @@
from jupyter_ydoc import ydocs as YDOCS
from jupyter_ydoc.ybasedoc import YBaseDoc
from pycrdt import Doc
-from pycrdt_websocket.ystore import BaseYStore
+from pycrdt.store import BaseYStore
from traitlets import Bool, Float, Type
from .handlers import (
@@ -50,6 +50,14 @@
saving changes from the front-end.""",
)
+ file_stop_poll_on_errors_after = Float(
+ 24 * 60 * 60,
+ allow_none=True,
+ config=True,
+ help="""The duration in seconds to stop polling a file after
consecutive errors.
+ Defaults to 24 hours, if None then polling will not stop on errors.""",
+ )
+
document_cleanup_delay = Float(
60,
allow_none=True,
@@ -121,7 +129,10 @@
# the global app settings in which the file id manager will register
# itself maybe at a later time.
self.file_loaders = FileLoaderMapping(
- self.serverapp.web_app.settings, self.log, self.file_poll_interval
+ self.serverapp.web_app.settings,
+ self.log,
+ self.file_poll_interval,
+ file_stop_poll_on_errors_after=self.file_stop_poll_on_errors_after,
)
self.handlers.extend(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/handlers.py
new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/handlers.py
--- old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/handlers.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/handlers.py
2020-02-02 01:00:00.000000000 +0100
@@ -16,8 +16,8 @@
from jupyter_server.utils import ensure_async
from jupyter_ydoc import ydocs as YDOCS
from pycrdt import Doc, Encoder, UndoManager
-from pycrdt_websocket.yroom import YRoom
-from pycrdt_websocket.ystore import BaseYStore
+from pycrdt.store import BaseYStore
+from pycrdt.websocket import YRoom
from tornado import web
from tornado.websocket import WebSocketHandler
@@ -120,7 +120,7 @@
updates_file_path = f".{file_type}:{file_id}.y"
ystore = self._ystore_class(
path=updates_file_path,
- log=self.log, # type:ignore[call-arg]
+ log=self.log,
)
self.room = DocumentRoom(
self._room_id,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/loaders.py
new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/loaders.py
--- old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/loaders.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/loaders.py
2020-02-02 01:00:00.000000000 +0100
@@ -5,7 +5,10 @@
import asyncio
from logging import Logger, getLogger
+from time import time
from typing import Any, Callable, Coroutine
+from tornado.web import HTTPError
+from http import HTTPStatus
from jupyter_server.services.contents.manager import (
AsyncContentsManager,
@@ -29,12 +32,16 @@
contents_manager: AsyncContentsManager | ContentsManager,
log: Logger | None = None,
poll_interval: float | None = None,
+ max_consecutive_logs: int = 3,
+ stop_poll_on_errors_after: float | None = None,
) -> None:
self._file_id: str = file_id
self._lock = asyncio.Lock()
self._poll_interval = poll_interval
+ self._stop_poll_on_errors_after = stop_poll_on_errors_after
self._file_id_manager = file_id_manager
+ self._max_consecutive_logs = max_consecutive_logs
self._contents_manager = contents_manager
self._log = log or getLogger(__name__)
@@ -79,7 +86,7 @@
try:
await self._watcher
except asyncio.CancelledError:
- self._log.info(f"file watcher for '{self.file_id}' is
cancelled now")
+ self._log.info(f"File watcher for '{self.file_id}' was
cancelled")
def observe(
self,
@@ -125,6 +132,14 @@
model = await ensure_async(
self._contents_manager.get(self.path, format=format,
type=file_type, content=True)
)
+ if (
+ file_type == "file"
+ and "content" in model
+ and model["content"]
+ and "\r\n" in model["content"]
+ ):
+ model["content"] = model["content"].replace("\r\n", "\n")
+ self._log.debug("Normalizing line endings for %s file on
content load", self.path)
self.last_modified = model["last_modified"]
return model
@@ -204,8 +219,8 @@
return
consecutive_error_logs = 0
- max_consecutive_logs = 3
suppression_logged = False
+ consecutive_errors_started = None
while True:
try:
@@ -214,13 +229,37 @@
await self.maybe_notify()
consecutive_error_logs = 0
suppression_logged = False
+ consecutive_errors_started = None
except Exception as e:
- if consecutive_error_logs < max_consecutive_logs:
- self._log.error(f"Error watching file:
{self.path}\n{e!r}", exc_info=e)
+ # We do not want to terminate the watcher if the content
manager request
+ # fails due to timeout, server error or similar temporary
issue; we only
+ # terminate if the file is not found or we get
unauthorized error for
+ # an extended period of time.
+ if isinstance(e, HTTPError) and e.status_code in {
+ HTTPStatus.NOT_FOUND,
+ HTTPStatus.UNAUTHORIZED,
+ }:
+ if (
+ consecutive_errors_started
+ and self._stop_poll_on_errors_after is not None
+ ):
+ errors_duration = time() -
consecutive_errors_started
+ if errors_duration >
self._stop_poll_on_errors_after:
+ self._log.warning(
+ "Stopping watching file due to consecutive
errors over %s seconds: %s",
+ self._stop_poll_on_errors_after,
+ self.path,
+ )
+ break
+ else:
+ consecutive_errors_started = time()
+ # Otherwise we just log the error
+ if consecutive_error_logs < self._max_consecutive_logs:
+ self._log.error("Error watching file %s: %s",
self.path, e, exc_info=e)
consecutive_error_logs += 1
elif not suppression_logged:
self._log.warning(
- "Too many errors while watching %s — suppressing
further logs.",
+ "Too many errors while watching %s - suppressing
further logs.",
self.path,
)
suppression_logged = True
@@ -268,6 +307,7 @@
settings: dict,
log: Logger | None = None,
file_poll_interval: float | None = None,
+ file_stop_poll_on_errors_after: float | None = None,
) -> None:
"""
Args:
@@ -279,6 +319,7 @@
self.__dict: dict[str, FileLoader] = {}
self.log = log or getLogger(__name__)
self.file_poll_interval = file_poll_interval
+ self._stop_poll_on_errors_after = file_stop_poll_on_errors_after
@property
def contents_manager(self) -> AsyncContentsManager | ContentsManager:
@@ -309,6 +350,7 @@
self.contents_manager,
self.log,
self.file_poll_interval,
+ stop_poll_on_errors_after=self._stop_poll_on_errors_after,
)
self.__dict[file_id] = file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/pytest_plugin.py
new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/pytest_plugin.py
--- old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/pytest_plugin.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/pytest_plugin.py
2020-02-02 01:00:00.000000000 +0100
@@ -14,13 +14,13 @@
from jupyter_server_ydoc.rooms import DocumentRoom
from jupyter_server_ydoc.stores import SQLiteYStore
from jupyter_ydoc import YNotebook, YUnicode
-from pycrdt_websocket import WebsocketProvider
+from pycrdt import Provider
+from pycrdt.websocket.websocket import HttpxWebsocket
from .test_utils import (
FakeContentsManager,
FakeEventLogger,
FakeFileIDManager,
- Websocket,
)
@@ -30,7 +30,14 @@
@pytest.fixture
-def jp_server_config(jp_root_dir, jp_server_config, rtc_document_save_delay):
+def rtc_document_cleanup_delay():
+ return 60
+
+
[email protected]
+def jp_server_config(
+ jp_root_dir, jp_server_config, rtc_document_save_delay,
rtc_document_cleanup_delay
+):
return {
"ServerApp": {
"jpserver_extensions": {
@@ -47,7 +54,10 @@
"db_path": str(jp_root_dir.joinpath(".fid_test.db")),
"db_journal_mode": "OFF",
},
- "YDocExtension": {"document_save_delay": rtc_document_save_delay},
+ "YDocExtension": {
+ "document_save_delay": rtc_document_save_delay,
+ "document_cleanup_delay": rtc_document_cleanup_delay,
+ },
}
@@ -231,7 +241,7 @@
doc.observe(_on_document_change)
websocket, room_name = await rtc_connect_doc_client(format, type, path)
- async with websocket as ws, WebsocketProvider(doc.ydoc, Websocket(ws,
room_name)):
+ async with websocket as ws, Provider(doc.ydoc, HttpxWebsocket(ws,
room_name)):
await event.wait()
await sleep(0.1)
@@ -243,7 +253,7 @@
db = SQLiteYStore(
path=f"{type}:{path}",
# `SQLiteYStore` here is a subclass of booth `LoggingConfigurable`
- # and `pycrdt_websocket.ystore.SQLiteYStore`, but mypy gets lost:
+ # and `pycrdt.store.SQLiteYStore`, but mypy gets lost:
config=jp_serverapp.config, # type:ignore[call-arg]
)
_ = create_task(db.start())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/rooms.py
new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/rooms.py
--- old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/rooms.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/rooms.py 2020-02-02
01:00:00.000000000 +0100
@@ -9,8 +9,8 @@
from jupyter_events import EventLogger
from jupyter_ydoc import ydocs as YDOCS
-from pycrdt_websocket.yroom import YRoom
-from pycrdt_websocket.ystore import BaseYStore, YDocNotFound
+from pycrdt.websocket import YRoom
+from pycrdt.store import BaseYStore, YDocNotFound
from .loaders import FileLoader
from .utils import JUPYTER_COLLABORATION_EVENTS_URI, LogLevel, OutOfBandChanges
@@ -278,20 +278,28 @@
return
self._saving_document = asyncio.create_task(
- self._maybe_save_document(self._saving_document)
+ self._maybe_save_document(self._saving_document, save_now=True)
)
return self._saving_document
- async def _maybe_save_document(self, saving_document: asyncio.Task | None)
-> None:
+ async def _maybe_save_document(
+ self, saving_document: asyncio.Task | None, save_now: bool = False
+ ) -> None:
"""
Saves the content of the document to disk.
### Note:
There is a save delay to debounce the save since we could receive
a high
amount of changes in a short period of time. This way we can
cancel the
- previous save.
+ previous save. When save_now is True, the delay is skipped and the
save
+ executes immediately.
+
+ Parameters:
+ saving_document: The previous saving task to cancel if needed.
+ save_now: If True, skip the debounce delay, and save
immediately.
+ This is used when manually saving.
"""
- if self._save_delay is None:
+ if self._save_delay is None and not save_now:
return
if saving_document is not None and not saving_document.done():
# the document is being saved, cancel that
@@ -301,8 +309,10 @@
# because this coroutine is run in a cancellable task and cancellation
is handled here
try:
- # save after X seconds of inactivity
- await asyncio.sleep(self._save_delay)
+ # When save_now is False, wait X seconds of inactivity before
saving (auto-save).
+ # When save_now is True, save immediately without debounce delay
(manual save).
+ if not save_now and self._save_delay is not None:
+ await asyncio.sleep(self._save_delay)
self.log.info("Saving the content from room %s", self._room_id)
saved_model = await self._file.maybe_save_content(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/stores.py
new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/stores.py
--- old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/stores.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/stores.py 2020-02-02
01:00:00.000000000 +0100
@@ -1,13 +1,17 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
-from pycrdt_websocket.ystore import SQLiteYStore as _SQLiteYStore
-from pycrdt_websocket.ystore import TempFileYStore as _TempFileYStore
+from pycrdt.store import SQLiteYStore as _SQLiteYStore
+from pycrdt.store import TempFileYStore as _TempFileYStore
from traitlets import Int, Unicode
from traitlets.config import LoggingConfigurable
-class TempFileYStore(_TempFileYStore):
+class TempFileYStoreMetaclass(type(LoggingConfigurable),
type(_TempFileYStore)): # type: ignore
+ pass
+
+
+class TempFileYStore(LoggingConfigurable, _TempFileYStore,
metaclass=TempFileYStoreMetaclass):
prefix_dir = "jupyter_ystore_"
@@ -23,10 +27,17 @@
directory.""",
)
- document_ttl = Int(
+ squash_after_inactivity_of = Int(
None,
allow_none=True,
config=True,
help="""The document time-to-live in seconds. Defaults to None
(document history is never
cleared).""",
)
+ document_ttl = Int(
+ None,
+ allow_none=True,
+ config=True,
+ help="""The document time-to-live in seconds. Deprecated in favor of
'squash_after_inactivity_of'.
+ Defaults to None (document history is never cleared).""",
+ )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/test_utils.py
new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/test_utils.py
--- old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/test_utils.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/test_utils.py
2020-02-02 01:00:00.000000000 +0100
@@ -5,8 +5,8 @@
from datetime import datetime
from typing import Any
+from tornado.web import HTTPError
-from anyio import Lock
from jupyter_server import _tz as tz
@@ -33,14 +33,22 @@
"mimetype": None,
"size": 0,
"writable": False,
+ "hash": "fake_hash",
}
self.model.update(model)
self.actions: list[str] = []
def get(
- self, path: str, content: bool = True, format: str | None = None,
type: str | None = None
+ self,
+ path: str,
+ content: bool = True,
+ format: str | None = None,
+ type: str | None = None,
+ require_hash: bool | None = None,
) -> dict:
+ if not self.model:
+ raise HTTPError(404, f"File not found: {path}")
self.actions.append("get")
return self.model
@@ -56,32 +64,3 @@
class FakeEventLogger:
def emit(self, schema_id: str, data: dict) -> None:
print(data)
-
-
-class Websocket:
- def __init__(self, websocket: Any, path: str):
- self._websocket = websocket
- self._path = path
- self._send_lock = Lock()
-
- @property
- def path(self) -> str:
- return self._path
-
- def __aiter__(self):
- return self
-
- async def __anext__(self) -> bytes:
- try:
- message = await self.recv()
- except Exception:
- raise StopAsyncIteration()
- return message
-
- async def send(self, message: bytes) -> None:
- async with self._send_lock:
- await self._websocket.send_bytes(message)
-
- async def recv(self) -> bytes:
- b = await self._websocket.receive_bytes()
- return bytes(b)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/websocketserver.py
new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/websocketserver.py
--- old/jupyter_server_ydoc-2.1.0/jupyter_server_ydoc/websocketserver.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/jupyter_server_ydoc/websocketserver.py
2020-02-02 01:00:00.000000000 +0100
@@ -7,10 +7,9 @@
from logging import Logger
from typing import Any, Callable
-from pycrdt_websocket.websocket import Websocket
-from pycrdt_websocket.websocket_server import WebsocketServer
-from pycrdt_websocket.yroom import YRoom
-from pycrdt_websocket.ystore import BaseYStore
+from pycrdt import Channel
+from pycrdt.store import BaseYStore
+from pycrdt.websocket import WebsocketServer, YRoom
class RoomNotFound(LookupError):
@@ -133,7 +132,7 @@
await self.start_room(room)
return room
- async def serve(self, websocket: Websocket) -> None:
+ async def serve(self, websocket: Channel) -> None:
# start monitoring here as the event loop is not yet available when
initializing the object
if self.monitor_task is None:
self.monitor_task = asyncio.create_task(self._monitor())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server_ydoc-2.1.0/pyproject.toml
new/jupyter_server_ydoc-2.2.1/pyproject.toml
--- old/jupyter_server_ydoc-2.1.0/pyproject.toml 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/pyproject.toml 2020-02-02
01:00:00.000000000 +0100
@@ -29,9 +29,9 @@
]
dependencies = [
"jupyter_server>=2.15.0,<3.0.0",
- "jupyter_ydoc>=2.1.2,<4.0.0,!=3.0.0,!=3.0.1",
+ "jupyter_ydoc>=3.0.3,<4.0.0",
"pycrdt",
- "pycrdt-websocket>=0.15.0,<0.16.0",
+ "pycrdt-websocket>=0.16.0,<0.17.0",
"jupyter_events>=0.11.0",
"jupyter_server_fileid>=0.7.0,<1",
"jsonschema>=4.18.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server_ydoc-2.1.0/tests/test_app.py
new/jupyter_server_ydoc-2.2.1/tests/test_app.py
--- old/jupyter_server_ydoc-2.1.0/tests/test_app.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/tests/test_app.py 2020-02-02
01:00:00.000000000 +0100
@@ -74,7 +74,23 @@
rtc_create_SQLite_store = rtc_create_SQLite_store_factory(app)
store = await rtc_create_SQLite_store("file", id, content)
- assert store.document_ttl == 3600
+ # document_ttl is deprecated and mapped to squash_after_inactivity_of
+ assert store.squash_after_inactivity_of == 3600
+
+
+async def test_squash_after_inactivity_of_from_settings(
+ rtc_create_mock_document_room, jp_configurable_serverapp
+):
+ argv = ["--SQLiteYStore.squash_after_inactivity_of=3600"]
+
+ app = jp_configurable_serverapp(argv=argv)
+
+ id = "test-id"
+ content = "test_ttl"
+ rtc_create_SQLite_store = rtc_create_SQLite_store_factory(app)
+ store = await rtc_create_SQLite_store("file", id, content)
+
+ assert store.squash_after_inactivity_of == 3600
@pytest.mark.parametrize("copy", [True, False])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server_ydoc-2.1.0/tests/test_documents.py
new/jupyter_server_ydoc-2.2.1/tests/test_documents.py
--- old/jupyter_server_ydoc-2.1.0/tests/test_documents.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/tests/test_documents.py 2020-02-02
01:00:00.000000000 +0100
@@ -11,8 +11,8 @@
import pytest
from anyio import create_task_group, sleep
-from jupyter_server_ydoc.test_utils import Websocket
-from pycrdt_websocket import WebsocketProvider
+from pycrdt import Provider
+from pycrdt.websocket.websocket import HttpxWebsocket
jupyter_ydocs = {ep.name: ep.load() for ep in
entry_points(group="jupyter_ydoc")}
@@ -34,7 +34,7 @@
jupyter_ydoc = jupyter_ydocs[file_type]()
websocket, room_name = await rtc_connect_doc_client(file_format,
file_type, file_path)
- async with websocket as ws, WebsocketProvider(jupyter_ydoc.ydoc,
Websocket(ws, room_name)):
+ async with websocket as ws, Provider(jupyter_ydoc.ydoc, HttpxWebsocket(ws,
room_name)):
for _ in range(2):
jupyter_ydoc.dirty = True
await sleep(rtc_document_save_delay * 1.5)
@@ -69,7 +69,8 @@
tg.start_soon(connect, file_format, file_type, file_path)
tg.start_soon(connect, file_format, file_type, file_path)
t1 = time()
- assert t1 - t0 < 0.5
+ delta = t1 - t0
+ assert delta < 0.6
await cleanup(jp_serverapp)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server_ydoc-2.1.0/tests/test_handlers.py
new/jupyter_server_ydoc-2.2.1/tests/test_handlers.py
--- old/jupyter_server_ydoc-2.1.0/tests/test_handlers.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/tests/test_handlers.py 2020-02-02
01:00:00.000000000 +0100
@@ -4,15 +4,15 @@
from __future__ import annotations
import json
+import pytest
from asyncio import Event, sleep
from typing import Any
from dirty_equals import IsStr
from jupyter_events.logger import EventLogger
-from jupyter_server_ydoc.test_utils import Websocket
from jupyter_ydoc import YUnicode
-from pycrdt import Text
-from pycrdt_websocket import WebsocketProvider
+from pycrdt import Text, Provider
+from pycrdt.websocket.websocket import HttpxWebsocket
async def test_session_handler_should_create_session_id(
@@ -81,7 +81,7 @@
doc.observe(_on_document_change)
websocket, room_name = await rtc_connect_doc_client("text", "file", path)
- async with websocket as ws, WebsocketProvider(doc.ydoc, Websocket(ws,
room_name)):
+ async with websocket as ws, Provider(doc.ydoc, HttpxWebsocket(ws,
room_name)):
await event.wait()
await sleep(0.1)
@@ -117,7 +117,7 @@
)
websocket, room_name = await rtc_connect_doc_client("text", "file", path)
- async with websocket as ws, WebsocketProvider(doc.ydoc, Websocket(ws,
room_name)):
+ async with websocket as ws, Provider(doc.ydoc, HttpxWebsocket(ws,
room_name)):
await event.wait()
await sleep(0.1)
@@ -134,6 +134,71 @@
assert collected_data[1]["username"] is not None
[email protected]
+def rtc_document_cleanup_delay():
+ return 2
+
+
+async def test_room_handler_doc_client_should_stop_file_watcher(
+ rtc_create_file, rtc_connect_doc_client, jp_serverapp
+):
+ path, _ = await rtc_create_file("test.txt", "test")
+ fim = jp_serverapp.web_app.settings["file_id_manager"]
+ file_loaders =
jp_serverapp.web_app.settings["jupyter_server_ydoc"].file_loaders
+
+ event = Event()
+
+ def _on_document_change(target: str, e: Any) -> None:
+ if target == "source":
+ event.set()
+
+ doc = YUnicode()
+ doc.observe(_on_document_change)
+
+ websocket, room_name = await rtc_connect_doc_client("text", "file", path)
+ async with websocket as ws, Provider(doc.ydoc, HttpxWebsocket(ws,
room_name)):
+ await event.wait()
+ file_id = fim.get_id("test.txt")
+ assert file_id in file_loaders
+ file_loader = file_loaders[file_id]
+ await sleep(0.1)
+
+ listener_was_called = False
+ collected_data = []
+
+ async def my_listener(logger: EventLogger, schema_id: str, data: dict) ->
None:
+ nonlocal listener_was_called
+ collected_data.append(data)
+ listener_was_called = True
+
+ event_logger = jp_serverapp.event_logger
+ event_logger.add_listener(
+
schema_id="https://schema.jupyter.org/jupyter_collaboration/session/v1",
+ listener=my_listener,
+ )
+
+ file_watcher = file_loader._watcher
+
+ # Before cleanup delay, the file watcher should still be running
+ assert not file_watcher.done()
+
+ # Wait for the cleanup delay (2 seconds) plus a buffer (0.5 seconds)
+ await sleep(2.5)
+
+ assert listener_was_called is True
+ assert len(collected_data) == 2
+ assert collected_data[0]["msg"] == "Room deleted."
+ assert collected_data[0]["path"] == "test.txt"
+ assert collected_data[1]["msg"] == "Loader deleted."
+ assert collected_data[1]["path"] == "test.txt"
+
+ # After the cleanup delay, the file watcher should be done
+ assert file_watcher.done()
+
+ await jp_serverapp.web_app.settings["jupyter_server_ydoc"].stop_extension()
+ del jp_serverapp.web_app.settings["file_id_manager"]
+
+
async def test_room_handler_doc_client_should_cleanup_room_file(
rtc_create_file, rtc_connect_doc_client, jp_serverapp
):
@@ -149,7 +214,7 @@
doc.observe(_on_document_change)
websocket, room_name = await rtc_connect_doc_client("text", "file", path)
- async with websocket as ws, WebsocketProvider(doc.ydoc, Websocket(ws,
room_name)):
+ async with websocket as ws, Provider(doc.ydoc, HttpxWebsocket(ws,
room_name)):
await event.wait()
await sleep(0.1)
@@ -174,7 +239,7 @@
try:
websocket, room_name = await rtc_connect_doc_client("text2", "file2",
path2)
- async with websocket as ws, WebsocketProvider(doc.ydoc, Websocket(ws,
room_name)):
+ async with websocket as ws, Provider(doc.ydoc, HttpxWebsocket(ws,
room_name)):
await event.wait()
await sleep(0.1)
except Exception:
@@ -182,7 +247,7 @@
try:
websocket, room_name = await rtc_connect_doc_client("text2", "file2",
path2)
- async with websocket as ws, WebsocketProvider(doc.ydoc, Websocket(ws,
room_name)):
+ async with websocket as ws, Provider(doc.ydoc, HttpxWebsocket(ws,
room_name)):
await event.wait()
await sleep(0.1)
except Exception:
@@ -253,7 +318,7 @@
root_roomid = f"text:file:{file_id}"
websocket, room_name = await rtc_connect_doc_client("text", "file", path)
- async with websocket as ws, WebsocketProvider(root_ydoc.ydoc,
Websocket(ws, room_name)):
+ async with websocket as ws, Provider(root_ydoc.ydoc, HttpxWebsocket(ws,
room_name)):
await root_connect_event.wait()
resp = await rtc_create_fork_client(root_roomid, False, "my fork0",
"is awesome0")
@@ -316,8 +381,8 @@
fork_ydoc.observe(_on_fork_change)
fork_text = fork_ydoc.ydoc.get("source", type=Text)
- async with await rtc_connect_fork_client(fork_roomid1) as ws,
WebsocketProvider(
- fork_ydoc.ydoc, Websocket(ws, fork_roomid1)
+ async with await rtc_connect_fork_client(fork_roomid1) as ws, Provider(
+ fork_ydoc.ydoc, HttpxWebsocket(ws, fork_roomid1)
):
await fork_connect_event.wait()
root_text = root_ydoc.ydoc.get("source", type=Text)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server_ydoc-2.1.0/tests/test_loaders.py
new/jupyter_server_ydoc-2.2.1/tests/test_loaders.py
--- old/jupyter_server_ydoc-2.1.0/tests/test_loaders.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/tests/test_loaders.py 2020-02-02
01:00:00.000000000 +0100
@@ -4,6 +4,7 @@
from __future__ import annotations
import asyncio
+import logging
from datetime import datetime, timedelta, timezone
from jupyter_server_ydoc.loaders import FileLoader, FileLoaderMapping
@@ -43,6 +44,66 @@
await loader.clean()
+async def test_FileLoader_with_watcher_errors(caplog):
+ id = "file-4567"
+ path = "myfile.txt"
+ paths = {}
+ paths[id] = path
+
+ cm = FakeContentsManager({"last_modified": datetime.now(timezone.utc)})
+
+ loader = FileLoader(
+ id,
+ FakeFileIDManager(paths),
+ cm,
+ poll_interval=0.1,
+ max_consecutive_logs=2,
+ stop_poll_on_errors_after=1,
+ )
+ await loader.load_content("text", "file")
+
+ try:
+ cm.model = {}
+ await asyncio.sleep(0.5)
+ logs = [r.getMessage() for r in caplog.records]
+ assert logs == [
+ "Error watching file myfile.txt: HTTP 404: Not Found (File not
found: myfile.txt)",
+ "Error watching file myfile.txt: HTTP 404: Not Found (File not
found: myfile.txt)",
+ "Too many errors while watching myfile.txt - suppressing further
logs.",
+ ]
+
+ await asyncio.sleep(1)
+ logs = [r.getMessage() for r in caplog.records]
+ assert len(logs) == 4
+ assert (
+ logs[-1]
+ == "Stopping watching file due to consecutive errors over 1
seconds: myfile.txt"
+ )
+ finally:
+ await loader.clean()
+
+
+async def test_FileLoader_clean_logs_cancellation(caplog):
+ id = "file-4567"
+ path = "myfile.txt"
+ paths = {id: path}
+
+ cm = FakeContentsManager({"last_modified": datetime.now(timezone.utc)})
+ loader = FileLoader(
+ id,
+ FakeFileIDManager(paths),
+ cm,
+ poll_interval=0.05,
+ )
+ await loader.load_content("text", "file")
+
+ caplog.set_level(logging.INFO)
+ await loader.clean()
+
+ messages = [r.getMessage() for r in caplog.records]
+ assert f"File watcher for '{id}' was cancelled" in messages
+
+
async def test_FileLoader_without_watcher():
id = "file-4567"
path = "myfile.txt"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server_ydoc-2.1.0/tests/test_rooms.py
new/jupyter_server_ydoc-2.2.1/tests/test_rooms.py
--- old/jupyter_server_ydoc-2.1.0/tests/test_rooms.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server_ydoc-2.2.1/tests/test_rooms.py 2020-02-02
01:00:00.000000000 +0100
@@ -121,6 +121,109 @@
assert "save" in cm.actions
+async def test_manual_save_should_not_have_delay(
+ rtc_create_mock_document_room,
+):
+ content = "test"
+ cm, _, room = rtc_create_mock_document_room("test-id", "test.txt",
content, save_delay=0.5)
+
+ await room.initialize()
+
+ # Trigger a manual save
+ room._save_to_disc()
+
+ # Manual save should execute immediately, without waiting for the 0.5s
delay
+ # Check that save happens within a very short time (100ms should be enough)
+ await asyncio.sleep(0.1)
+
+ assert cm.actions.count("save") == 1
+
+
+async def test_manual_save_with_pending_autosave_should_cancel_autosave(
+ rtc_create_mock_document_room,
+):
+ content = "test"
+ cm, _, room = rtc_create_mock_document_room("test-id", "test.txt",
content, save_delay=1.0)
+
+ await room.initialize()
+
+ room._document.source = "Test 2"
+
+ await asyncio.sleep(0.1)
+
+ assert cm.actions.count("save") == 0
+
+ save_task = room._save_to_disc()
+
+ # Manual save should execute immediately
+ await asyncio.sleep(0.1)
+ assert save_task.done()
+
+ # Check that the manual save was recorded
+ assert cm.actions.count("save") == 1
+
+ await asyncio.sleep(1.0)
+
+ # There should be only one save (the manual one), not two
+ assert cm.actions.count("save") == 1
+
+
+async def test_manual_save_should_execute_immediately_even_with_long_delay(
+ rtc_create_mock_document_room,
+):
+ content = "test"
+ cm, _, room = rtc_create_mock_document_room("test-id", "test.txt",
content, save_delay=5.0)
+
+ await room.initialize()
+
+ save_task = room._save_to_disc()
+
+ await asyncio.sleep(0.5)
+
+ assert "save" in cm.actions
+ assert save_task.done()
+
+
+async def test_autosave_should_still_have_delay(
+ rtc_create_mock_document_room,
+):
+ content = "test"
+ save_delay = 0.3
+ cm, _, room = rtc_create_mock_document_room(
+ "test-id", "test.txt", content, save_delay=save_delay
+ )
+
+ await room.initialize()
+
+ room._document.source = "Test 3"
+
+ await asyncio.sleep(0.1)
+ assert "save" not in cm.actions
+
+ # Wait for the delay to complete
+ await asyncio.sleep(save_delay)
+
+ assert "save" in cm.actions
+
+
+async def
test_manual_save_should_work_when_save_delay_is_none_and_save_now_is_true(
+ rtc_create_mock_document_room,
+):
+ """Test that manual saves execute even when save_delay is None."""
+ content = "test"
+ # When save_delay is None, autosave is disabled
+ cm, _, room = rtc_create_mock_document_room("test-id", "test.txt",
content, save_delay=None)
+
+ await room.initialize()
+
+ # Trigger a manual save with save_now=True
+ # Even though save_delay is None, manual saves should still work
+ await room._maybe_save_document(None, save_now=True)
+
+ # Manual save should have executed
+ assert cm.actions.count("save") == 1
+
+
# The following test should be restored when package versions are fixed.
# async def test_document_path(rtc_create_mock_document_room):