Skip to content

Commit 283c41c

Browse files
authored
Defer preferred_dir validation until root_dir is set (#826)
1 parent 196ee87 commit 283c41c

File tree

3 files changed

+97
-6
lines changed

3 files changed

+97
-6
lines changed

jupyter_server/pytest_plugin.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,13 +249,18 @@ def _configurable_serverapp(
249249
c = Config(config)
250250
c.NotebookNotary.db_file = ":memory:"
251251
token = hexlify(os.urandom(4)).decode("ascii")
252+
253+
# Allow tests to configure root_dir via a file, argv, or its
254+
# default (cwd) by specifying a value of None.
255+
if root_dir is not None:
256+
kwargs["root_dir"] = str(root_dir)
257+
252258
app = ServerApp.instance(
253259
# Set the log level to debug for testing purposes
254260
log_level="DEBUG",
255261
port=http_port,
256262
port_retries=0,
257263
open_browser=False,
258-
root_dir=str(root_dir),
259264
base_url=base_url,
260265
config=c,
261266
allow_root=True,

jupyter_server/serverapp.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,11 +1605,22 @@ def _normalize_dir(self, value):
16051605
value = os.path.abspath(value)
16061606
return value
16071607

1608+
# Because the validation of preferred_dir depends on root_dir and validation
1609+
# occurs when the trait is loaded, there are times when we should defer the
1610+
# validation of preferred_dir (e.g., when preferred_dir is defined via CLI
1611+
# and root_dir is defined via a config file).
1612+
_defer_preferred_dir_validation = False
1613+
16081614
@validate("root_dir")
16091615
def _root_dir_validate(self, proposal):
16101616
value = self._normalize_dir(proposal["value"])
16111617
if not os.path.isdir(value):
16121618
raise TraitError(trans.gettext("No such directory: '%r'") % value)
1619+
1620+
if self._defer_preferred_dir_validation:
1621+
# If we're here, then preferred_dir is configured on the CLI and
1622+
# root_dir is configured in a file
1623+
self._preferred_dir_validation(self.preferred_dir, value)
16131624
return value
16141625

16151626
preferred_dir = Unicode(
@@ -1627,16 +1638,28 @@ def _preferred_dir_validate(self, proposal):
16271638
if not os.path.isdir(value):
16281639
raise TraitError(trans.gettext("No such preferred dir: '%r'") % value)
16291640

1630-
# preferred_dir must be equal or a subdir of root_dir
1631-
if not value.startswith(self.root_dir):
1641+
# Before we validate against root_dir, check if this trait is defined on the CLI
1642+
# and root_dir is not. If that's the case, we'll defer it's further validation
1643+
# until root_dir is validated or the server is starting (the latter occurs when
1644+
# the default root_dir (cwd) is used).
1645+
cli_config = self.cli_config.get("ServerApp", {})
1646+
if "preferred_dir" in cli_config and "root_dir" not in cli_config:
1647+
self._defer_preferred_dir_validation = True
1648+
1649+
if not self._defer_preferred_dir_validation: # Validate now
1650+
self._preferred_dir_validation(value, self.root_dir)
1651+
return value
1652+
1653+
def _preferred_dir_validation(self, preferred_dir: str, root_dir: str) -> None:
1654+
"""Validate preferred dir relative to root_dir - preferred_dir must be equal or a subdir of root_dir"""
1655+
if not preferred_dir.startswith(root_dir):
16321656
raise TraitError(
16331657
trans.gettext(
16341658
"preferred_dir must be equal or a subdir of root_dir. preferred_dir: '%r' root_dir: '%r'"
16351659
)
1636-
% (value, self.root_dir)
1660+
% (preferred_dir, root_dir)
16371661
)
1638-
1639-
return value
1662+
self._defer_preferred_dir_validation = False
16401663

16411664
@observe("root_dir")
16421665
def _root_dir_changed(self, change):
@@ -2377,6 +2400,10 @@ def initialize(
23772400
# Parse command line, load ServerApp config files,
23782401
# and update ServerApp config.
23792402
super().initialize(argv=argv)
2403+
if self._defer_preferred_dir_validation:
2404+
# If we're here, then preferred_dir is configured on the CLI and
2405+
# root_dir has the default value (cwd)
2406+
self._preferred_dir_validation(self.preferred_dir, self.root_dir)
23802407
if self._dispatching:
23812408
return
23822409
# Then, use extensions' config loading mechanism to

tests/test_serverapp.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,65 @@ def test_valid_preferred_dir_does_not_exist(tmp_path, jp_configurable_serverapp)
281281
assert "No such preferred dir:" in str(error)
282282

283283

284+
@pytest.mark.parametrize(
285+
"root_dir_loc,preferred_dir_loc",
286+
[
287+
("cli", "cli"),
288+
("cli", "config"),
289+
("cli", "default"),
290+
("config", "cli"),
291+
("config", "config"),
292+
("config", "default"),
293+
("default", "cli"),
294+
("default", "config"),
295+
("default", "default"),
296+
],
297+
)
298+
def test_preferred_dir_validation(
299+
root_dir_loc, preferred_dir_loc, tmp_path, jp_config_dir, jp_configurable_serverapp
300+
):
301+
expected_root_dir = str(tmp_path)
302+
expected_preferred_dir = str(tmp_path / "subdir")
303+
os.makedirs(expected_preferred_dir, exist_ok=True)
304+
305+
argv = []
306+
kwargs = {"root_dir": None}
307+
308+
config_lines = []
309+
config_file = None
310+
if root_dir_loc == "config" or preferred_dir_loc == "config":
311+
config_file = jp_config_dir.joinpath("jupyter_server_config.py")
312+
313+
if root_dir_loc == "cli":
314+
argv.append(f"--ServerApp.root_dir={expected_root_dir}")
315+
if root_dir_loc == "config":
316+
config_lines.append(f'c.ServerApp.root_dir = r"{expected_root_dir}"')
317+
if root_dir_loc == "default":
318+
expected_root_dir = os.getcwd()
319+
320+
if preferred_dir_loc == "cli":
321+
argv.append(f"--ServerApp.preferred_dir={expected_preferred_dir}")
322+
if preferred_dir_loc == "config":
323+
config_lines.append(f'c.ServerApp.preferred_dir = r"{expected_preferred_dir}"')
324+
if preferred_dir_loc == "default":
325+
expected_preferred_dir = expected_root_dir
326+
327+
if config_file is not None:
328+
config_file.write_text("\n".join(config_lines))
329+
330+
if argv:
331+
kwargs["argv"] = argv
332+
333+
if root_dir_loc == "default" and preferred_dir_loc != "default": # error expected
334+
with pytest.raises(SystemExit):
335+
jp_configurable_serverapp(**kwargs)
336+
else:
337+
app = jp_configurable_serverapp(**kwargs)
338+
assert app.root_dir == expected_root_dir
339+
assert app.preferred_dir == expected_preferred_dir
340+
assert app.preferred_dir.startswith(app.root_dir)
341+
342+
284343
def test_invalid_preferred_dir_does_not_exist(tmp_path, jp_configurable_serverapp):
285344
path = str(tmp_path)
286345
path_subdir = str(tmp_path / "subdir")

0 commit comments

Comments
 (0)