From bc86e4c8f51d533836be89eeb385ac61d90b49b0 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Sat, 8 Jul 2023 11:29:59 +0200 Subject: [PATCH 1/2] Backport PR #4503: set root_dir when using singleuser extension --- jupyterhub/singleuser/extension.py | 5 ++ jupyterhub/tests/extension/__init__.py | 1 + jupyterhub/tests/test_singleuser.py | 78 +++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/jupyterhub/singleuser/extension.py b/jupyterhub/singleuser/extension.py index f5e2f197..b0ba550b 100644 --- a/jupyterhub/singleuser/extension.py +++ b/jupyterhub/singleuser/extension.py @@ -483,6 +483,11 @@ class JupyterHubSingleUser(ExtensionApp): cfg.answer_yes = True self.config.FileContentsManager.delete_to_trash = False + # load Spawner.notebook_dir configuration, if given + root_dir = os.getenv("JUPYTERHUB_ROOT_DIR", None) + if root_dir: + cfg.root_dir = os.path.expanduser(root_dir) + # load http server config from environment url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL']) if url.port: diff --git a/jupyterhub/tests/extension/__init__.py b/jupyterhub/tests/extension/__init__.py index a8c307ac..d83301b0 100644 --- a/jupyterhub/tests/extension/__init__.py +++ b/jupyterhub/tests/extension/__init__.py @@ -30,6 +30,7 @@ class JupyterHubTestHandler(JupyterHandler): info = { "current_user": self.current_user, "config": self.app.config, + "root_dir": self.contents_manager.root_dir, "disable_user_config": getattr(self.app, "disable_user_config", None), "settings": self.settings, "config_file_paths": self.app.config_file_paths, diff --git a/jupyterhub/tests/test_singleuser.py b/jupyterhub/tests/test_singleuser.py index da8356a2..54150837 100644 --- a/jupyterhub/tests/test_singleuser.py +++ b/jupyterhub/tests/test_singleuser.py @@ -2,6 +2,7 @@ import os import sys from contextlib import nullcontext +from pprint import pprint from subprocess import CalledProcessError, check_output from unittest import mock from urllib.parse import urlencode, urlparse @@ -171,9 +172,7 @@ async def test_disable_user_config(request, app, tmpdir, full_spawn): ) r.raise_for_status() info = r.json() - import pprint - - pprint.pprint(info) + pprint(info) assert info['disable_user_config'] server_config = info['config'] settings = info['settings'] @@ -198,6 +197,79 @@ async def test_disable_user_config(request, app, tmpdir, full_spawn): assert_not_in_home(path, key) +@pytest.mark.parametrize("extension", [True, False]) +@pytest.mark.parametrize("notebook_dir", ["", "~", "~/sub", "ABS"]) +async def test_notebook_dir( + request, app, tmpdir, user, full_spawn, extension, notebook_dir +): + if extension: + try: + import jupyter_server # noqa + except ImportError: + pytest.skip("needs jupyter-server 2") + else: + if jupyter_server.version_info < (2,): + pytest.skip("needs jupyter-server 2") + + token = user.new_api_token(scopes=["access:servers!user"]) + headers = {"Authorization": f"Bearer {token}"} + + spawner = user.spawner + if extension: + user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "1" + else: + user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "0" + + home_dir = tmpdir.join("home").mkdir() + sub_dir = home_dir.join("sub").mkdir() + with sub_dir.join("subfile.txt").open("w") as f: + f.write("txt\n") + abs_dir = tmpdir.join("abs").mkdir() + with abs_dir.join("absfile.txt").open("w") as f: + f.write("absfile\n") + + if notebook_dir: + expected_root_dir = notebook_dir.replace("ABS", str(abs_dir)).replace( + "~", str(home_dir) + ) + else: + expected_root_dir = str(home_dir) + + spawner.notebook_dir = notebook_dir.replace("ABS", str(abs_dir)) + + # home_dir is defined on SimpleSpawner + user.spawner.home_dir = home = str(home_dir) + spawner.environment["HOME"] = home + await user.spawn() + await app.proxy.add_user(user) + url = public_url(app, user) + r = await async_requests.get( + url_path_join(public_url(app, user), 'jupyterhub-test-info'), headers=headers + ) + r.raise_for_status() + info = r.json() + pprint(info) + + assert info["root_dir"] == expected_root_dir + # secondary check: make sure it has the intended effect on root_dir + r = await async_requests.get( + url_path_join(public_url(app, user), 'api/contents/'), headers=headers + ) + r.raise_for_status() + root_contents = sorted(item['name'] for item in r.json()['content']) + + # check contents + if not notebook_dir or notebook_dir == "~": + # use any to avoid counting possible automatically created files in $HOME + assert 'sub' in root_contents + elif notebook_dir == "ABS": + assert 'absfile.txt' in root_contents + elif notebook_dir == "~/sub": + assert 'subfile.txt' in root_contents + else: + raise ValueError(f"No contents check for {notebook_dir=}") + + def test_help_output(): out = check_output( [sys.executable, '-m', 'jupyterhub.singleuser', '--help-all'] From a9dc5884549ee0555e3f9647d1e7b5785b89b37f Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 9 Aug 2023 11:56:03 +0200 Subject: [PATCH 2/2] can't use f"{name=}" in Python 3.7 --- jupyterhub/tests/test_singleuser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/tests/test_singleuser.py b/jupyterhub/tests/test_singleuser.py index 54150837..926106a4 100644 --- a/jupyterhub/tests/test_singleuser.py +++ b/jupyterhub/tests/test_singleuser.py @@ -267,7 +267,7 @@ async def test_notebook_dir( elif notebook_dir == "~/sub": assert 'subfile.txt' in root_contents else: - raise ValueError(f"No contents check for {notebook_dir=}") + raise ValueError(f"No contents check for {notebook_dir}") def test_help_output():