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):

Reply via email to