diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 8ac457bed1..8e18c5642a 100755 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -532,6 +532,7 @@ def start(self): 'certfile': 'ServerApp.certfile', 'client-ca': 'ServerApp.client_ca', 'notebook-dir': 'ServerApp.root_dir', + 'preferred-dir': 'ServerApp.preferred_dir', 'browser': 'ServerApp.browser', 'pylab': 'ServerApp.pylab', 'gateway-url': 'GatewayClient.url', @@ -1138,22 +1139,54 @@ def _default_root_dir(self): else: return py3compat.getcwd() - @validate('root_dir') - def _root_dir_validate(self, proposal): - value = proposal['value'] + def _normalize_root_or_preferred_dir(self, value): # Strip any trailing slashes # *except* if it's root _, path = os.path.splitdrive(value) if path == os.sep: return value + value = value.rstrip(os.sep) if not os.path.isabs(value): # If we receive a non-absolute path, make it absolute. value = os.path.abspath(value) + + return value + + @validate('root_dir') + def _root_dir_validate(self, proposal): + value = self._normalize_root_or_preferred_dir(proposal['value']) if not os.path.isdir(value): raise TraitError(trans.gettext("No such notebook dir: '%r'") % value) + return value + preferred_dir = Unicode(config=True, + help=_("Prefered starting directory to use for notebooks and kernels.") + ) + + @default('preferred_dir') + def _default_prefered_dir(self): + return self.root_dir + + @validate('preferred_dir') + def _preferred_dir_validate(self, proposal): + value = self._normalize_root_or_preferred_dir(proposal['value']) + if not os.path.isdir(value): + raise TraitError(trans.gettext("No such preferred dir: '%r'") % value) + + # preferred_dir must be equal or a subdir of root_dir + if not value.startswith(self.root_dir): + raise TraitError(trans.gettext("preferred_dir must be equal or a subdir of root_dir: '%r'") % value) + + return value + + @observe('root_dir') + def _update_root_dir(self, change): + if not self.preferred_dir.startswith(change['new']): + self.log.warning(_("Value of preferred_dir updated to use value of root_dir")) + self.preferred_dir = change['new'] + @observe('server_extensions') def _update_server_extensions(self, change): self.log.warning(_("server_extensions is deprecated, use jpserver_extensions")) @@ -1204,7 +1237,6 @@ def _update_server_extensions(self, change): """)) def parse_command_line(self, argv=None): - super(ServerApp, self).parse_command_line(argv) if self.extra_args: @@ -1222,6 +1254,7 @@ def parse_command_line(self, argv=None): c.ServerApp.root_dir = f elif os.path.isfile(f): c.ServerApp.file_to_run = f + self.update_config(c) def init_configurables(self): diff --git a/tests/test_serverapp.py b/tests/test_serverapp.py index 8b1ed09bd3..6a5e794dad 100644 --- a/tests/test_serverapp.py +++ b/tests/test_serverapp.py @@ -123,3 +123,92 @@ def test_list_running_servers(serverapp, app): servers = list(list_running_servers(serverapp.runtime_dir)) assert len(servers) >= 1 + +# Preferred dir tests +# ---------------------------------------------------------------------------- +def test_valid_preferred_dir(tmp_path, configurable_serverapp): + path = str(tmp_path) + app = configurable_serverapp(root_dir=path, preferred_dir=path) + assert app.root_dir == path + assert app.preferred_dir == path + assert app.root_dir == app.preferred_dir + + +def test_valid_preferred_dir_is_root_subdir(tmp_path, configurable_serverapp): + path = str(tmp_path) + path_subdir = str(tmp_path / 'subdir') + os.makedirs(path_subdir, exist_ok=True) + app = configurable_serverapp(root_dir=path, preferred_dir=path_subdir) + assert app.root_dir == path + assert app.preferred_dir == path_subdir + assert app.preferred_dir.startswith(app.root_dir) + + +def test_valid_preferred_dir_does_not_exist(tmp_path, configurable_serverapp): + path = str(tmp_path) + path_subdir = str(tmp_path / 'subdir') + with pytest.raises(TraitError) as error: + app = configurable_serverapp(root_dir=path, preferred_dir=path_subdir) + + assert "No such preferred dir:" in str(error) + + +def test_invalid_preferred_dir_does_not_exist(tmp_path, configurable_serverapp): + path = str(tmp_path) + path_subdir = str(tmp_path / 'subdir') + with pytest.raises(TraitError) as error: + app = configurable_serverapp(root_dir=path, preferred_dir=path_subdir) + + assert "No such preferred dir:" in str(error) + + +def test_invalid_preferred_dir_does_not_exist_set(tmp_path, configurable_serverapp): + path = str(tmp_path) + path_subdir = str(tmp_path / 'subdir') + + app = configurable_serverapp(root_dir=path) + with pytest.raises(TraitError) as error: + app.preferred_dir = path_subdir + + assert "No such preferred dir:" in str(error) + + +def test_invalid_preferred_dir_not_root_subdir(tmp_path, configurable_serverapp): + path = str(tmp_path / 'subdir') + os.makedirs(path, exist_ok=True) + not_subdir_path = str(tmp_path) + + with pytest.raises(TraitError) as error: + app = configurable_serverapp(root_dir=path, preferred_dir=not_subdir_path) + + assert "preferred_dir must be equal or a subdir of root_dir:" in str(error) + + +def test_invalid_preferred_dir_not_root_subdir_set(tmp_path, configurable_serverapp): + path = str(tmp_path / 'subdir') + os.makedirs(path, exist_ok=True) + not_subdir_path = str(tmp_path) + + app = configurable_serverapp(root_dir=path) + with pytest.raises(TraitError) as error: + app.preferred_dir = not_subdir_path + + assert "preferred_dir must be equal or a subdir of root_dir:" in str(error) + + +def test_observed_root_dir_updates_preferred_dir(tmp_path, configurable_serverapp): + path = str(tmp_path) + new_path = str(tmp_path / 'subdir') + os.makedirs(new_path, exist_ok=True) + + app = configurable_serverapp(root_dir=path, preferred_dir=path) + app.root_dir = new_path + assert app.preferred_dir == new_path + + +def test_observed_root_dir_does_not_update_preferred_dir(tmp_path, configurable_serverapp): + path = str(tmp_path) + new_path = str(tmp_path.parent) + app = configurable_serverapp(root_dir=path, preferred_dir=path) + app.root_dir = new_path + assert app.preferred_dir == path