diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index dd9939ca5d..49e9687d76 100755 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -610,6 +610,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', @@ -1355,9 +1356,7 @@ def _default_root_dir(self): else: return os.getcwd() - @validate('root_dir') - def _root_dir_validate(self, proposal): - value = proposal['value'] + def _normalize_dir(self, value): # Strip any trailing slashes # *except* if it's root _, path = os.path.splitdrive(value) @@ -1367,13 +1366,41 @@ def _root_dir_validate(self, proposal): 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_dir(proposal['value']) if not os.path.isdir(value): raise TraitError(trans.gettext("No such directory: '%r'") % value) return value + preferred_dir = Unicode(config=True, + help=trans.gettext("Preferred 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_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 _root_dir_changed(self, change): self._root_dir_set = True + if not self.preferred_dir.startswith(change['new']): + self.log.warning(trans.gettext("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): diff --git a/jupyter_server/tests/test_serverapp.py b/jupyter_server/tests/test_serverapp.py index a74b731469..a850abd067 100644 --- a/jupyter_server/tests/test_serverapp.py +++ b/jupyter_server/tests/test_serverapp.py @@ -286,3 +286,93 @@ def test_urls(config, public_url, local_url, connection_url): assert serverapp.connection_url == connection_url # Cleanup singleton after test. ServerApp.clear_instance() + + +# Preferred dir tests +# ---------------------------------------------------------------------------- +def test_valid_preferred_dir(tmp_path, jp_configurable_serverapp): + path = str(tmp_path) + app = jp_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, jp_configurable_serverapp): + path = str(tmp_path) + path_subdir = str(tmp_path / 'subdir') + os.makedirs(path_subdir, exist_ok=True) + app = jp_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, jp_configurable_serverapp): + path = str(tmp_path) + path_subdir = str(tmp_path / 'subdir') + with pytest.raises(TraitError) as error: + app = jp_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, jp_configurable_serverapp): + path = str(tmp_path) + path_subdir = str(tmp_path / 'subdir') + with pytest.raises(TraitError) as error: + app = jp_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, jp_configurable_serverapp): + path = str(tmp_path) + path_subdir = str(tmp_path / 'subdir') + + app = jp_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, jp_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 = jp_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, jp_configurable_serverapp): + path = str(tmp_path / 'subdir') + os.makedirs(path, exist_ok=True) + not_subdir_path = str(tmp_path) + + app = jp_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, jp_configurable_serverapp): + path = str(tmp_path) + new_path = str(tmp_path / 'subdir') + os.makedirs(new_path, exist_ok=True) + + app = jp_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, jp_configurable_serverapp): + path = str(tmp_path) + new_path = str(tmp_path.parent) + app = jp_configurable_serverapp(root_dir=path, preferred_dir=path) + app.root_dir = new_path + assert app.preferred_dir == path