Skip to content

Commit 999b056

Browse files
Backport PR #826 on branch 1.x (Defer preferred_dir validation until root_dir is set) (#828)
Co-authored-by: Kevin Bates <[email protected]>
1 parent 2e31f90 commit 999b056

File tree

3 files changed

+97
-6
lines changed

3 files changed

+97
-6
lines changed

jupyter_server/pytest_plugin.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,18 @@ def _configurable_serverapp(
238238
c = Config(config)
239239
c.NotebookNotary.db_file = ":memory:"
240240
token = hexlify(os.urandom(4)).decode("ascii")
241+
242+
# Allow tests to configure root_dir via a file, argv, or its
243+
# default (cwd) by specifying a value of None.
244+
if root_dir is not None:
245+
kwargs["root_dir"] = str(root_dir)
246+
241247
app = ServerApp.instance(
242248
# Set the log level to debug for testing purposes
243249
log_level="DEBUG",
244250
port=http_port,
245251
port_retries=0,
246252
open_browser=False,
247-
root_dir=str(root_dir),
248253
base_url=base_url,
249254
config=c,
250255
allow_root=True,

jupyter_server/serverapp.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -1593,11 +1593,22 @@ def _normalize_dir(self, value):
15931593
value = os.path.abspath(value)
15941594
return value
15951595

1596+
# Because the validation of preferred_dir depends on root_dir and validation
1597+
# occurs when the trait is loaded, there are times when we should defer the
1598+
# validation of preferred_dir (e.g., when preferred_dir is defined via CLI
1599+
# and root_dir is defined via a config file).
1600+
_defer_preferred_dir_validation = False
1601+
15961602
@validate("root_dir")
15971603
def _root_dir_validate(self, proposal):
15981604
value = self._normalize_dir(proposal["value"])
15991605
if not os.path.isdir(value):
16001606
raise TraitError(trans.gettext("No such directory: '%r'") % value)
1607+
1608+
if self._defer_preferred_dir_validation:
1609+
# If we're here, then preferred_dir is configured on the CLI and
1610+
# root_dir is configured in a file
1611+
self._preferred_dir_validation(self.preferred_dir, value)
16011612
return value
16021613

16031614
preferred_dir = Unicode(
@@ -1615,16 +1626,28 @@ def _preferred_dir_validate(self, proposal):
16151626
if not os.path.isdir(value):
16161627
raise TraitError(trans.gettext("No such preferred dir: '%r'") % value)
16171628

1618-
# preferred_dir must be equal or a subdir of root_dir
1619-
if not value.startswith(self.root_dir):
1629+
# Before we validate against root_dir, check if this trait is defined on the CLI
1630+
# and root_dir is not. If that's the case, we'll defer it's further validation
1631+
# until root_dir is validated or the server is starting (the latter occurs when
1632+
# the default root_dir (cwd) is used).
1633+
cli_config = self.cli_config.get("ServerApp", {})
1634+
if "preferred_dir" in cli_config and "root_dir" not in cli_config:
1635+
self._defer_preferred_dir_validation = True
1636+
1637+
if not self._defer_preferred_dir_validation: # Validate now
1638+
self._preferred_dir_validation(value, self.root_dir)
1639+
return value
1640+
1641+
def _preferred_dir_validation(self, preferred_dir: str, root_dir: str) -> None:
1642+
"""Validate preferred dir relative to root_dir - preferred_dir must be equal or a subdir of root_dir"""
1643+
if not preferred_dir.startswith(root_dir):
16201644
raise TraitError(
16211645
trans.gettext(
16221646
"preferred_dir must be equal or a subdir of root_dir. preferred_dir: '%r' root_dir: '%r'"
16231647
)
1624-
% (value, self.root_dir)
1648+
% (preferred_dir, root_dir)
16251649
)
1626-
1627-
return value
1650+
self._defer_preferred_dir_validation = False
16281651

16291652
@observe("root_dir")
16301653
def _root_dir_changed(self, change):
@@ -2387,6 +2410,10 @@ def initialize(
23872410
# Parse command line, load ServerApp config files,
23882411
# and update ServerApp config.
23892412
super().initialize(argv=argv)
2413+
if self._defer_preferred_dir_validation:
2414+
# If we're here, then preferred_dir is configured on the CLI and
2415+
# root_dir has the default value (cwd)
2416+
self._preferred_dir_validation(self.preferred_dir, self.root_dir)
23902417
if self._dispatching:
23912418
return
23922419
# Then, use extensions' config loading mechanism to

tests/test_serverapp.py

+59
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)