Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add file system change notifier #783

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion jupyter_server/services/contents/filemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from datetime import datetime

import nbformat
import watchfiles
from anyio.to_thread import run_sync
from jupyter_core.paths import exists, is_file_hidden, is_hidden
from send2trash import send2trash
Expand All @@ -22,7 +23,7 @@

from .filecheckpoints import AsyncFileCheckpoints, FileCheckpoints
from .fileio import AsyncFileManagerMixin, FileManagerMixin
from .manager import AsyncContentsManager, ContentsManager
from .manager import AsyncContentsManager, BaseWatchfiles, ContentsManager

try:
from os.path import samefile
Expand All @@ -33,8 +34,16 @@
_script_exporter = None


class Watchfiles(BaseWatchfiles):
watch = watchfiles.watch
awatch = watchfiles.awatch
Change = watchfiles.Change


class FileContentsManager(FileManagerMixin, ContentsManager):

watchfiles = Watchfiles

root_dir = Unicode(config=True)

@default("root_dir")
Expand Down Expand Up @@ -546,6 +555,9 @@ def get_kernel_path(self, path, model=None):


class AsyncFileContentsManager(FileContentsManager, AsyncFileManagerMixin, AsyncContentsManager):

watchfiles = Watchfiles

@default("checkpoints_class")
def _checkpoints_class_default(self):
return AsyncFileCheckpoints
Expand Down
18 changes: 18 additions & 0 deletions jupyter_server/services/contents/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import re
import warnings
from enum import IntEnum
from fnmatch import fnmatch

from nbformat import ValidationError, sign
Expand Down Expand Up @@ -34,6 +35,21 @@
copy_pat = re.compile(r"\-Copy\d*\.")


class BaseWatchfiles:
"""File system change notifyer API

Override these attributes in subclasses if the file system supports file change notifications.
"""

def watch(*paths, **kwargs):
raise NotImplementedError

async def awatch(*paths, **kwargs):
raise NotImplementedError

Change = IntEnum


class ContentsManager(LoggingConfigurable):
"""Base class for serving files and directories.

Expand Down Expand Up @@ -409,6 +425,8 @@ def rename_file(self, old_path, new_path):
# ContentsManager API part 2: methods that have useable default
# implementations, but can be overridden in subclasses.

watchfiles = BaseWatchfiles

def delete(self, path):
"""Delete a file/directory and any associated checkpoints."""
path = path.strip("/")
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ install_requires =
terminado>=0.8.3
tornado>=6.1.0
traitlets>=5.1
watchfiles==0.13
websocket-client

[options.extras_require]
Expand Down
28 changes: 28 additions & 0 deletions tests/services/contents/test_largefilemanager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import asyncio

import pytest
import tornado

Expand Down Expand Up @@ -110,3 +112,29 @@ async def test_save_in_subdirectory(jp_large_contents_manager, tmp_path):
assert "path" in model
assert model["name"] == "Untitled.ipynb"
assert model["path"] == "foo/Untitled.ipynb"


async def test_watch_directory(tmp_path):
cm = AsyncLargeFileManager(root_dir=str(tmp_path))
file_path = tmp_path / "file.txt"
stop_event = asyncio.Event()

async def change_dir():
# let the watcher start
await asyncio.sleep(0.1)
# add file to directory
file_path.write_text("foo")

async def timeout():
await asyncio.sleep(10)
stop_event.set()

asyncio.create_task(change_dir())
asyncio.create_task(timeout())
test_ok = False
async for change in cm.watchfiles.awatch(tmp_path, stop_event=stop_event):
if (cm.watchfiles.Change.added, str(file_path)) in change:
test_ok = True
break

assert test_ok