Skip to content

Commit 2ca8008

Browse files
vidartfblink1073
andauthored
Backport PR #1162: Reapply preferred_dir fix, now with better backwards compatability (#1167)
Co-authored-by: Steven Silvester <[email protected]>
1 parent 36e2100 commit 2ca8008

File tree

8 files changed

+142
-94
lines changed

8 files changed

+142
-94
lines changed

jupyter_server/serverapp.py

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,22 +1629,11 @@ def _normalize_dir(self, value):
16291629
value = os.path.abspath(value)
16301630
return value
16311631

1632-
# Because the validation of preferred_dir depends on root_dir and validation
1633-
# occurs when the trait is loaded, there are times when we should defer the
1634-
# validation of preferred_dir (e.g., when preferred_dir is defined via CLI
1635-
# and root_dir is defined via a config file).
1636-
_defer_preferred_dir_validation = False
1637-
16381632
@validate("root_dir")
16391633
def _root_dir_validate(self, proposal):
16401634
value = self._normalize_dir(proposal["value"])
16411635
if not os.path.isdir(value):
16421636
raise TraitError(trans.gettext("No such directory: '%r'") % value)
1643-
1644-
if self._defer_preferred_dir_validation:
1645-
# If we're here, then preferred_dir is configured on the CLI and
1646-
# root_dir is configured in a file
1647-
self._preferred_dir_validation(self.preferred_dir, value)
16481637
return value
16491638

16501639
preferred_dir = Unicode(
@@ -1661,39 +1650,8 @@ def _preferred_dir_validate(self, proposal):
16611650
value = self._normalize_dir(proposal["value"])
16621651
if not os.path.isdir(value):
16631652
raise TraitError(trans.gettext("No such preferred dir: '%r'") % value)
1664-
1665-
# Before we validate against root_dir, check if this trait is defined on the CLI
1666-
# and root_dir is not. If that's the case, we'll defer it's further validation
1667-
# until root_dir is validated or the server is starting (the latter occurs when
1668-
# the default root_dir (cwd) is used).
1669-
cli_config = self.cli_config.get("ServerApp", {})
1670-
if "preferred_dir" in cli_config and "root_dir" not in cli_config:
1671-
self._defer_preferred_dir_validation = True
1672-
1673-
if not self._defer_preferred_dir_validation: # Validate now
1674-
self._preferred_dir_validation(value, self.root_dir)
16751653
return value
16761654

1677-
def _preferred_dir_validation(self, preferred_dir: str, root_dir: str) -> None:
1678-
"""Validate preferred dir relative to root_dir - preferred_dir must be equal or a subdir of root_dir"""
1679-
if not preferred_dir.startswith(root_dir):
1680-
raise TraitError(
1681-
trans.gettext(
1682-
"preferred_dir must be equal or a subdir of root_dir. preferred_dir: '%r' root_dir: '%r'"
1683-
)
1684-
% (preferred_dir, root_dir)
1685-
)
1686-
self._defer_preferred_dir_validation = False
1687-
1688-
@observe("root_dir")
1689-
def _root_dir_changed(self, change):
1690-
self._root_dir_set = True
1691-
if not self.preferred_dir.startswith(change["new"]):
1692-
self.log.warning(
1693-
trans.gettext("Value of preferred_dir updated to use value of root_dir")
1694-
)
1695-
self.preferred_dir = change["new"]
1696-
16971655
@observe("server_extensions")
16981656
def _update_server_extensions(self, change):
16991657
self.log.warning(_i18n("server_extensions is deprecated, use jpserver_extensions"))
@@ -1868,6 +1826,9 @@ def init_configurables(self):
18681826
parent=self,
18691827
log=self.log,
18701828
)
1829+
# Trigger a default/validation here explicitly while we still support the
1830+
# deprecated trait on ServerApp (FIXME remove when deprecation finalized)
1831+
self.contents_manager.preferred_dir
18711832
self.session_manager = self.session_manager_class(
18721833
parent=self,
18731834
log=self.log,
@@ -2432,10 +2393,6 @@ def initialize(
24322393
# Parse command line, load ServerApp config files,
24332394
# and update ServerApp config.
24342395
super().initialize(argv=argv)
2435-
if self._defer_preferred_dir_validation:
2436-
# If we're here, then preferred_dir is configured on the CLI and
2437-
# root_dir has the default value (cwd)
2438-
self._preferred_dir_validation(self.preferred_dir, self.root_dir)
24392396
if self._dispatching:
24402397
return
24412398
# Then, use extensions' config loading mechanism to

jupyter_server/services/contents/fileio.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ def _get_os_path(self, path):
254254
404: if path is outside root
255255
"""
256256
root = os.path.abspath(self.root_dir)
257+
# to_os_path is not safe if path starts with a drive, since os.path.join discards first part
258+
if os.path.splitdrive(path)[0]:
259+
raise HTTPError(404, "%s is not a relative API path" % path)
257260
os_path = to_os_path(path, root)
258261
if not (os.path.abspath(os_path) + os.path.sep).startswith(root):
259262
raise HTTPError(404, "%s is outside root contents directory" % path)

jupyter_server/services/contents/filemanager.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import shutil
88
import stat
99
import sys
10+
import warnings
1011
from datetime import datetime
12+
from pathlib import Path
1113

1214
import nbformat
1315
from anyio.to_thread import run_sync
@@ -19,6 +21,7 @@
1921
from jupyter_server import _tz as tz
2022
from jupyter_server.base.handlers import AuthenticatedFileHandler
2123
from jupyter_server.transutils import _i18n
24+
from jupyter_server.utils import to_api_path
2225

2326
from .filecheckpoints import AsyncFileCheckpoints, FileCheckpoints
2427
from .fileio import AsyncFileManagerMixin, FileManagerMixin
@@ -55,6 +58,34 @@ def _validate_root_dir(self, proposal):
5558
raise TraitError("%r is not a directory" % value)
5659
return value
5760

61+
@default("preferred_dir")
62+
def _default_preferred_dir(self):
63+
try:
64+
value = self.parent.preferred_dir
65+
if value == self.parent.root_dir:
66+
value = None
67+
except AttributeError:
68+
pass
69+
else:
70+
if value is not None:
71+
warnings.warn(
72+
"ServerApp.preferred_dir config is deprecated in jupyter-server 2.0. Use FileContentsManager.preferred_dir instead",
73+
FutureWarning,
74+
stacklevel=3,
75+
)
76+
try:
77+
path = Path(value)
78+
return path.relative_to(self.root_dir).as_posix()
79+
except ValueError:
80+
raise TraitError("%s is outside root contents directory" % value) from None
81+
return ""
82+
83+
@validate("preferred_dir")
84+
def _validate_preferred_dir(self, proposal):
85+
# It should be safe to pass an API path through this method:
86+
proposal["value"] = to_api_path(proposal["value"], self.root_dir)
87+
return super()._validate_preferred_dir(proposal)
88+
5889
@default("checkpoints_class")
5990
def _checkpoints_class_default(self):
6091
return FileCheckpoints

jupyter_server/services/contents/manager.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
# Distributed under the terms of the Modified BSD License.
44
import itertools
55
import json
6+
import os
67
import re
78
import warnings
89
from fnmatch import fnmatch
910

11+
from jupyter_core.utils import run_sync
1012
from nbformat import ValidationError, sign
1113
from nbformat import validate as validate_nb
1214
from nbformat.v4 import new_notebook
@@ -55,10 +57,40 @@ class ContentsManager(LoggingConfigurable):
5557

5658
root_dir = Unicode("/", config=True)
5759

60+
preferred_dir = Unicode(
61+
"",
62+
config=True,
63+
help=_i18n(
64+
"Preferred starting directory to use for notebooks. This is an API path (`/` separated, relative to root dir)"
65+
),
66+
)
67+
68+
@validate("preferred_dir")
69+
def _validate_preferred_dir(self, proposal):
70+
value = proposal["value"].strip("/")
71+
try:
72+
import inspect
73+
74+
if inspect.iscoroutinefunction(self.dir_exists):
75+
dir_exists = run_sync(self.dir_exists)(value)
76+
else:
77+
dir_exists = self.dir_exists(value)
78+
except HTTPError as e:
79+
raise TraitError(e.log_message) from e
80+
if not dir_exists:
81+
raise TraitError(_i18n("Preferred directory not found: %r") % value)
82+
try:
83+
if value != self.parent.preferred_dir:
84+
self.parent.preferred_dir = os.path.join(self.root_dir, *value.split("/"))
85+
except (AttributeError, TraitError):
86+
pass
87+
return value
88+
5889
allow_hidden = Bool(False, config=True, help="Allow access to hidden files")
5990

6091
notary = Instance(sign.NotebookNotary)
6192

93+
@default("notary")
6294
def _notary_default(self):
6395
return sign.NotebookNotary(parent=self)
6496

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,6 @@ module = [
103103
"websocket"
104104
]
105105
ignore_missing_imports = true
106+
107+
[tool.check-wheel-contents]
108+
ignore = ["W002"]

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ install_requires =
4242
argon2-cffi
4343
jinja2
4444
jupyter_client>=6.1.12
45-
jupyter_core>=4.7.0
45+
jupyter_core>=4.12,!=5.0.*
4646
nbconvert>=6.4.4
4747
nbformat>=5.2.0
4848
packaging

tests/test_gateway.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ async def test_gateway_class_mappings(init_gateway, jp_serverapp):
270270
assert jp_serverapp.kernel_spec_manager_class.__name__ == "GatewayKernelSpecManager"
271271

272272

273-
async def test_gateway_get_kernelspecs(init_gateway, jp_fetch):
273+
async def test_gateway_get_kernelspecs(init_gateway, jp_serverapp, jp_fetch):
274274
# Validate that kernelspecs come from gateway.
275275
with mocked_gateway:
276276
r = await jp_fetch("api", "kernelspecs", method="GET")
@@ -297,11 +297,11 @@ async def test_gateway_get_named_kernelspec(init_gateway, jp_fetch):
297297
assert expected_http_error(e, 404)
298298

299299

300-
async def test_gateway_session_lifecycle(init_gateway, jp_root_dir, jp_fetch):
300+
async def test_gateway_session_lifecycle(init_gateway, jp_fetch):
301301
# Validate session lifecycle functions; create and delete.
302302

303303
# create
304-
session_id, kernel_id = await create_session(jp_root_dir, jp_fetch, "kspec_foo")
304+
session_id, kernel_id = await create_session(jp_fetch, "kspec_foo")
305305

306306
# ensure kernel still considered running
307307
assert await is_kernel_running(jp_fetch, kernel_id) is True
@@ -447,12 +447,12 @@ async def test_channel_queue_get_msg_when_response_router_had_finished():
447447
#
448448
# Test methods below...
449449
#
450-
async def create_session(root_dir, jp_fetch, kernel_name):
450+
async def create_session(jp_fetch, kernel_name):
451451
"""Creates a session for a kernel. The session is created against the server
452452
which then uses the gateway for kernel management.
453453
"""
454454
with mocked_gateway:
455-
nb_path = root_dir / "testgw.ipynb"
455+
nb_path = "/testgw.ipynb"
456456
body = json.dumps(
457457
{"path": str(nb_path), "type": "notebook", "kernel": {"name": kernel_name}}
458458
)

0 commit comments

Comments
 (0)