diff --git a/jupyterfs/extension.py b/jupyterfs/extension.py index 9414a94..c120215 100644 --- a/jupyterfs/extension.py +++ b/jupyterfs/extension.py @@ -10,7 +10,7 @@ from jupyter_server.utils import url_path_join from ._version import __version__ # noqa: F401 -from .metamanager import MetaManager, MetaManagerHandler +from .metamanager import MetaManagerShared, MetaManager, MetaManagerHandler from .snippets import SnippetsHandler _mm_config_warning_msg = """Misconfiguration of MetaManager. Please add: @@ -37,12 +37,12 @@ def _load_jupyter_server_extension(serverapp): base_url = web_app.settings["base_url"] host_pattern = ".*$" - if not isinstance(serverapp.contents_manager, MetaManager): + if not isinstance(serverapp.contents_manager, MetaManagerShared): warnings.warn(_mm_config_warning_msg) return if isinstance(serverapp.contents_manager_class, type) and not issubclass( - serverapp.contents_manager_class, MetaManager + serverapp.contents_manager_class, MetaManagerShared ): serverapp.contents_manager_class = MetaManager serverapp.log.info( diff --git a/jupyterfs/metamanager.py b/jupyterfs/metamanager.py index 97792ac..2a9344f 100644 --- a/jupyterfs/metamanager.py +++ b/jupyterfs/metamanager.py @@ -15,7 +15,10 @@ from fs.errors import FSError from fs.opener.errors import OpenerError, ParseError from jupyter_server.base.handlers import APIHandler -from jupyter_server.services.contents.manager import AsyncContentsManager +from jupyter_server.services.contents.manager import ( + AsyncContentsManager, + ContentsManager, +) from .auth import substituteAsk, substituteEnv, substituteNone from .config import JupyterFs as JupyterFsConfig @@ -30,10 +33,10 @@ stripDrive, ) -__all__ = ["MetaManager", "MetaManagerHandler"] +__all__ = ["MetaManager", "SyncMetaManager", "MetaManagerHandler"] -class MetaManager(AsyncContentsManager): +class MetaManagerShared: copy_pat = re.compile(r"\-Copy\d*\.") @default("files_handler_params") @@ -153,7 +156,7 @@ def root_manager(self): def root_dir(self): return self.root_manager.root_dir - async def copy(self, from_path, to_path=None): + def copy(self, from_path, to_path=None): """Copy an existing file and return its new model. If to_path not specified, it will be the parent directory of from_path. @@ -205,30 +208,57 @@ def _getManagerForPath(self, path): return mgr, stripDrive(path) - is_hidden = path_first_arg("is_hidden", False) - dir_exists = path_first_arg("dir_exists", False) - file_exists = path_kwarg("file_exists", "", False) - exists = path_first_arg("exists", False) + is_hidden = path_first_arg("is_hidden", False, sync=False) + dir_exists = path_first_arg("dir_exists", False, sync=False) + file_exists = path_kwarg("file_exists", "", False, sync=False) + exists = path_first_arg("exists", False, sync=False) + + save = path_second_arg("save", "model", True, sync=False) + rename = path_old_new("rename", False, sync=False) + + get = path_first_arg("get", True, sync=False) + delete = path_first_arg("delete", False, sync=False) + + get_kernel_path = path_first_arg("get_kernel_path", False, sync=True) + + create_checkpoint = path_first_arg("create_checkpoint", False, sync=False) + list_checkpoints = path_first_arg("list_checkpoints", False, sync=False) + restore_checkpoint = path_second_arg( + "restore_checkpoint", "checkpoint_id", False, sync=False + ) + delete_checkpoint = path_second_arg( + "delete_checkpoint", "checkpoint_id", False, sync=False + ) + + +class SyncMetaManager(MetaManagerShared, ContentsManager): + ... + + +class MetaManager(MetaManagerShared, AsyncContentsManager): + async def copy(self, from_path, to_path=None): + return super(MetaManagerShared, self).copy(from_path=from_path, to_path=to_path) + + is_hidden = path_first_arg("is_hidden", False, sync=False) + dir_exists = path_first_arg("dir_exists", False, sync=False) + file_exists = path_kwarg("file_exists", "", False, sync=False) + exists = path_first_arg("exists", False, sync=False) - save = path_second_arg("save", "model", True) - rename = path_old_new("rename", False) + save = path_second_arg("save", "model", True, sync=False) + rename = path_old_new("rename", False, sync=False) - get = path_first_arg("get", True) - delete = path_first_arg("delete", False) + get = path_first_arg("get", True, sync=False) + delete = path_first_arg("delete", False, sync=False) get_kernel_path = path_first_arg("get_kernel_path", False, sync=True) - create_checkpoint = path_first_arg("create_checkpoint", False) - list_checkpoints = path_first_arg("list_checkpoints", False) + create_checkpoint = path_first_arg("create_checkpoint", False, sync=False) + list_checkpoints = path_first_arg("list_checkpoints", False, sync=False) restore_checkpoint = path_second_arg( - "restore_checkpoint", - "checkpoint_id", - False, + "restore_checkpoint", "checkpoint_id", False, sync=False ) delete_checkpoint = path_second_arg( - "delete_checkpoint", - "checkpoint_id", - False, + "delete_checkpoint", "checkpoint_id", False, sync=False ) diff --git a/jupyterfs/pathutils.py b/jupyterfs/pathutils.py index 65b8be7..fe00616 100644 --- a/jupyterfs/pathutils.py +++ b/jupyterfs/pathutils.py @@ -93,61 +93,69 @@ def path_first_arg(method_name, returns_model, sync=False): """Decorator for methods that accept path as a first argument, e.g. manager.get(path, ...)""" - if sync: - - def _wrapper(self, *args, **kwargs): - path, args = _get_arg("path", args, kwargs) - _, mgr, mgr_path = _resolve_path(path, self._managers) - result = getattr(mgr, method_name)(mgr_path, *args, **kwargs) - return result + def _wrapper(self, *args, **kwargs): + path, args = _get_arg("path", args, kwargs) + _, mgr, mgr_path = _resolve_path(path, self._managers) + result = getattr(mgr, method_name)(mgr_path, *args, **kwargs) + return result - else: + if sync: + return _wrapper - async def _wrapper(self, *args, **kwargs): - path, args = _get_arg("path", args, kwargs) - _, mgr, mgr_path = _resolve_path(path, self._managers) - result = getattr(mgr, method_name)(mgr_path, *args, **kwargs) - return result + async def _wrapper2(self, *args, **kwargs): + return _wrapper(self, *args, **kwargs) - return _wrapper + return _wrapper2 -def path_second_arg(method_name, first_argname, returns_model): +def path_second_arg(method_name, first_argname, returns_model, sync=False): """Decorator for methods that accept path as a second argument. e.g. manager.save(model, path, ...)""" - async def _wrapper(self, *args, **kwargs): + def _wrapper(self, *args, **kwargs): other, args = _get_arg(first_argname, args, kwargs) path, args = _get_arg("path", args, kwargs) _, mgr, mgr_path = _resolve_path(path, self._managers) result = getattr(mgr, method_name)(other, mgr_path, *args, **kwargs) return result - return _wrapper + if sync: + return _wrapper + + async def _wrapper2(self, *args, **kwargs): + return _wrapper(self, *args, **kwargs) + + return _wrapper2 -def path_kwarg(method_name, path_default, returns_model): +def path_kwarg(method_name, path_default, returns_model, sync=False): """Parameterized decorator for methods that accept path as a second argument. e.g. manager.file_exists(path='') """ - async def _wrapper(self, path=path_default, **kwargs): + def _wrapper(self, path=path_default, **kwargs): _, mgr, mgr_path = _resolve_path(path, self._managers) result = getattr(mgr, method_name)(path=mgr_path, **kwargs) return result - return _wrapper + if sync: + return _wrapper + + async def _wrapper2(self, *args, **kwargs): + return _wrapper(self, *args, **kwargs) + return _wrapper2 -def path_old_new(method_name, returns_model): + +def path_old_new(method_name, returns_model, sync=False): """Decorator for methods accepting old_path and new_path. e.g. manager.rename(old_path, new_path) """ - async def _wrapper(self, old_path, new_path, *args, **kwargs): + def _wrapper(self, old_path, new_path, *args, **kwargs): old_prefix, old_mgr, old_mgr_path = _resolve_path(old_path, self._managers) new_prefix, new_mgr, new_mgr_path = _resolve_path(new_path, self._managers) if old_mgr is not new_mgr: @@ -165,7 +173,13 @@ async def _wrapper(self, old_path, new_path, *args, **kwargs): ) return result - return _wrapper + if sync: + return _wrapper + + async def _wrapper2(self, *args, **kwargs): + return _wrapper(self, *args, **kwargs) + + return _wrapper2 # handlers for drive specifications in path strings, as in "fooDrive:bar/baz.buzz" diff --git a/jupyterfs/tests/test_metamanager.py b/jupyterfs/tests/test_metamanager.py index cd16f1a..87c20c8 100644 --- a/jupyterfs/tests/test_metamanager.py +++ b/jupyterfs/tests/test_metamanager.py @@ -20,6 +20,14 @@ "JupyterFs": {}, } +sync_base_config = { + "ServerApp": { + "jpserver_extensions": {"jupyterfs.extension": True}, + "contents_manager_class": "jupyterfs.metamanager.SyncMetaManager", + }, + "JupyterFs": {}, +} + deny_client_config = { "JupyterFs": { "allow_user_resources": False, @@ -40,7 +48,7 @@ def our_config(): @pytest.fixture -def jp_server_config(tmp_path, tmp_osfs_resource, our_config): +def jp_server_config(base_config, tmp_path, tmp_osfs_resource, our_config): c = Config(base_config) c.JupyterFs.setdefault("resources", []) if tmp_osfs_resource: @@ -54,6 +62,7 @@ def jp_server_config(tmp_path, tmp_osfs_resource, our_config): return c +@pytest.mark.parametrize("base_config", [base_config, sync_base_config]) @pytest.mark.parametrize("our_config", [deny_client_config]) async def test_client_creation_disallowed(tmp_path, jp_fetch, jp_server_config): cc = ContentsClient(jp_fetch) @@ -63,6 +72,7 @@ async def test_client_creation_disallowed(tmp_path, jp_fetch, jp_server_config): assert resources == [] +@pytest.mark.parametrize("base_config", [base_config, sync_base_config]) @pytest.mark.parametrize("our_config", [deny_client_config]) @pytest.mark.parametrize("tmp_osfs_resource", [True]) async def test_client_creation_disallowed_retains_server_config( @@ -76,6 +86,7 @@ async def test_client_creation_disallowed_retains_server_config( assert names == {"test-server-config"} +@pytest.mark.parametrize("base_config", [base_config, sync_base_config]) @pytest.mark.parametrize( "our_config", [ @@ -124,6 +135,7 @@ async def test_resource_validators(tmp_path, jp_fetch, jp_server_config): assert names == {"valid-1", "valid-2"} +@pytest.mark.parametrize("base_config", [base_config, sync_base_config]) @pytest.mark.parametrize( "our_config", [