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 5 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
5 changes: 5 additions & 0 deletions jupyter_server/services/contents/filemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@

class FileContentsManager(FileManagerMixin, ContentsManager):

import watchfiles

root_dir = Unicode(config=True)

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


class AsyncFileContentsManager(FileContentsManager, AsyncFileManagerMixin, AsyncContentsManager):

import watchfiles

@default("checkpoints_class")
def _checkpoints_class_default(self):
return AsyncFileCheckpoints
Expand Down
21 changes: 21 additions & 0 deletions jupyter_server/services/contents/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
copy_pat = re.compile(r"\-Copy\d*\.")


class NoWatchfilesAPI:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think NoWatchfilesAPI should contain the desired APIs without concrete implementations. So it can be used as a type hint for the watchfiles property of ContentsManager

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes so that would go in Martin's direction.

pass


NOWATCHFILESAPI = NoWatchfilesAPI()


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

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

@property
def watchfiles(self):
"""File system change notifyer

Override this method in subclasses if the file system supports change notifications.

Returns
-------
api : class
The supported API for file system change notifications. Loosely follows the API of the
watchfiles Python package (can be a subset of it).
"""
return NOWATCHFILESAPI

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