Skip to content

Commit f75e2e7

Browse files
committed
[Enhancement] Define a CURRENT_JUPYTER_HANDLER context var
1 parent 2d01f84 commit f75e2e7

File tree

2 files changed

+61
-0
lines changed

2 files changed

+61
-0
lines changed

jupyter_server/base/handlers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Distributed under the terms of the Modified BSD License.
44
from __future__ import annotations
55

6+
import contextvars
67
import functools
78
import inspect
89
import ipaddress
@@ -66,6 +67,9 @@ def log():
6667
return app_log
6768

6869

70+
CURRENT_JUPYTER_HANDLER = contextvars.ContextVar("CURRENT_JUPYTER_HANDLER")
71+
72+
6973
class AuthenticatedHandler(web.RequestHandler):
7074
"""A RequestHandler with an authenticated user."""
7175

@@ -580,6 +584,9 @@ def check_host(self):
580584

581585
async def prepare(self):
582586
"""Pepare a response."""
587+
# Set the current Jupyter Handler context variable.
588+
CURRENT_JUPYTER_HANDLER.set(self)
589+
583590
if not self.check_host():
584591
self.current_user = self._jupyter_current_user = None
585592
raise web.HTTPError(403)

tests/test_contextvars.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import asyncio
2+
import time
3+
4+
from jupyter_server.auth.utils import get_anonymous_username
5+
from jupyter_server.base.handlers import CURRENT_JUPYTER_HANDLER, JupyterHandler
6+
from jupyter_server.services.kernels.kernelmanager import AsyncMappingKernelManager
7+
8+
9+
async def test_jupyter_handler_contextvar(jp_fetch, monkeypatch):
10+
# Create some mock kernel Ids
11+
kernel1 = "x-x-x-x-x"
12+
kernel2 = "y-y-y-y-y"
13+
14+
# We'll use this dictionary to track the current user within each request.
15+
context_tracker = {
16+
kernel1: {"started": "no user yet", "ended": "still no user", "user": None},
17+
kernel2: {"started": "no user yet", "ended": "still no user", "user": None},
18+
}
19+
20+
# Monkeypatch the get_current_user method in Tornado's
21+
# request handler to return a random user name for
22+
# each request
23+
async def get_current_user(self):
24+
return get_anonymous_username()
25+
26+
monkeypatch.setattr(JupyterHandler, "get_current_user", get_current_user)
27+
28+
# Monkeypatch the kernel_model method to show that
29+
# the current context variable is truly local and
30+
# not contaminated by other asynchronous parallel requests.
31+
def kernel_model(self, kernel_id):
32+
# Get the Jupyter Handler from the current context.
33+
current: JupyterHandler = CURRENT_JUPYTER_HANDLER.get()
34+
# Get the current user
35+
context_tracker[kernel_id]["user"] = current.current_user
36+
context_tracker[kernel_id]["started"] = current.current_user
37+
time.sleep(2.0)
38+
# Track the current user a few seconds later. We'll
39+
# verify that this user was unaffected by other parallel
40+
# requests.
41+
context_tracker[kernel_id]["ended"] = current.current_user
42+
return {"id": kernel_id, "name": "blah"}
43+
44+
monkeypatch.setattr(AsyncMappingKernelManager, "kernel_model", kernel_model)
45+
46+
# Make two requests in parallel.
47+
await asyncio.gather(jp_fetch("api", "kernels", kernel1), jp_fetch("api", "kernels", kernel2))
48+
49+
# Assert that the two requests had different users
50+
assert context_tracker[kernel1]["user"] != context_tracker[kernel2]["user"]
51+
# Assert that the first request started+ended with the same user
52+
assert context_tracker[kernel1]["started"] == context_tracker[kernel1]["ended"]
53+
# Assert that the second request started+ended with the same user
54+
assert context_tracker[kernel2]["started"] == context_tracker[kernel2]["ended"]

0 commit comments

Comments
 (0)