From f75e2e7e5145ef43db11362e390b41b67f668d2a Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Wed, 29 Mar 2023 21:05:41 -0700 Subject: [PATCH 01/10] [Enhancement] Define a CURRENT_JUPYTER_HANDLER context var --- jupyter_server/base/handlers.py | 7 +++++ tests/test_contextvars.py | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/test_contextvars.py diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 8aec2fe128..e91f1e36c9 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -3,6 +3,7 @@ # Distributed under the terms of the Modified BSD License. from __future__ import annotations +import contextvars import functools import inspect import ipaddress @@ -66,6 +67,9 @@ def log(): return app_log +CURRENT_JUPYTER_HANDLER = contextvars.ContextVar("CURRENT_JUPYTER_HANDLER") + + class AuthenticatedHandler(web.RequestHandler): """A RequestHandler with an authenticated user.""" @@ -580,6 +584,9 @@ def check_host(self): async def prepare(self): """Pepare a response.""" + # Set the current Jupyter Handler context variable. + CURRENT_JUPYTER_HANDLER.set(self) + if not self.check_host(): self.current_user = self._jupyter_current_user = None raise web.HTTPError(403) diff --git a/tests/test_contextvars.py b/tests/test_contextvars.py new file mode 100644 index 0000000000..f1690e502d --- /dev/null +++ b/tests/test_contextvars.py @@ -0,0 +1,54 @@ +import asyncio +import time + +from jupyter_server.auth.utils import get_anonymous_username +from jupyter_server.base.handlers import CURRENT_JUPYTER_HANDLER, JupyterHandler +from jupyter_server.services.kernels.kernelmanager import AsyncMappingKernelManager + + +async def test_jupyter_handler_contextvar(jp_fetch, monkeypatch): + # Create some mock kernel Ids + kernel1 = "x-x-x-x-x" + kernel2 = "y-y-y-y-y" + + # We'll use this dictionary to track the current user within each request. + context_tracker = { + kernel1: {"started": "no user yet", "ended": "still no user", "user": None}, + kernel2: {"started": "no user yet", "ended": "still no user", "user": None}, + } + + # Monkeypatch the get_current_user method in Tornado's + # request handler to return a random user name for + # each request + async def get_current_user(self): + return get_anonymous_username() + + monkeypatch.setattr(JupyterHandler, "get_current_user", get_current_user) + + # Monkeypatch the kernel_model method to show that + # the current context variable is truly local and + # not contaminated by other asynchronous parallel requests. + def kernel_model(self, kernel_id): + # Get the Jupyter Handler from the current context. + current: JupyterHandler = CURRENT_JUPYTER_HANDLER.get() + # Get the current user + context_tracker[kernel_id]["user"] = current.current_user + context_tracker[kernel_id]["started"] = current.current_user + time.sleep(2.0) + # Track the current user a few seconds later. We'll + # verify that this user was unaffected by other parallel + # requests. + context_tracker[kernel_id]["ended"] = current.current_user + return {"id": kernel_id, "name": "blah"} + + monkeypatch.setattr(AsyncMappingKernelManager, "kernel_model", kernel_model) + + # Make two requests in parallel. + await asyncio.gather(jp_fetch("api", "kernels", kernel1), jp_fetch("api", "kernels", kernel2)) + + # Assert that the two requests had different users + assert context_tracker[kernel1]["user"] != context_tracker[kernel2]["user"] + # Assert that the first request started+ended with the same user + assert context_tracker[kernel1]["started"] == context_tracker[kernel1]["ended"] + # Assert that the second request started+ended with the same user + assert context_tracker[kernel2]["started"] == context_tracker[kernel2]["ended"] From e9084b3604bc35cc9380671e6348cafcab6759d7 Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Wed, 29 Mar 2023 21:24:10 -0700 Subject: [PATCH 02/10] add type to context var --- jupyter_server/base/handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index e91f1e36c9..c1a241a40f 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -3,7 +3,6 @@ # Distributed under the terms of the Modified BSD License. from __future__ import annotations -import contextvars import functools import inspect import ipaddress @@ -14,6 +13,7 @@ import traceback import types import warnings +from contextvars import ContextVar from http.client import responses from typing import TYPE_CHECKING, Awaitable from urllib.parse import urlparse @@ -67,7 +67,7 @@ def log(): return app_log -CURRENT_JUPYTER_HANDLER = contextvars.ContextVar("CURRENT_JUPYTER_HANDLER") +CURRENT_JUPYTER_HANDLER: ContextVar[JupyterHandler] = ContextVar("CURRENT_JUPYTER_HANDLER") class AuthenticatedHandler(web.RequestHandler): From fa6c772f6b5942679b83b3863f71688cd993121b Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Fri, 7 Apr 2023 17:00:41 -0700 Subject: [PATCH 03/10] Introduce CallContext class --- jupyter_server/base/handlers.py | 7 +-- .../services/sessions/call_context.py | 59 +++++++++++++++++++ .../sessions/test_call_context.py} | 56 +++++++++++++++++- 3 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 jupyter_server/services/sessions/call_context.py rename tests/{test_contextvars.py => services/sessions/test_call_context.py} (54%) diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index c1a241a40f..5c9bdd95c6 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -13,7 +13,6 @@ import traceback import types import warnings -from contextvars import ContextVar from http.client import responses from typing import TYPE_CHECKING, Awaitable from urllib.parse import urlparse @@ -32,6 +31,7 @@ from jupyter_server.auth import authorized from jupyter_server.i18n import combine_translations from jupyter_server.services.security import csp_report_uri +from jupyter_server.services.sessions.call_context import CallContext from jupyter_server.utils import ( ensure_async, filefind, @@ -67,9 +67,6 @@ def log(): return app_log -CURRENT_JUPYTER_HANDLER: ContextVar[JupyterHandler] = ContextVar("CURRENT_JUPYTER_HANDLER") - - class AuthenticatedHandler(web.RequestHandler): """A RequestHandler with an authenticated user.""" @@ -585,7 +582,7 @@ def check_host(self): async def prepare(self): """Pepare a response.""" # Set the current Jupyter Handler context variable. - CURRENT_JUPYTER_HANDLER.set(self) + CallContext.set(CallContext.JUPYTER_HANDLER, self) if not self.check_host(): self.current_user = self._jupyter_current_user = None diff --git a/jupyter_server/services/sessions/call_context.py b/jupyter_server/services/sessions/call_context.py new file mode 100644 index 0000000000..b643809595 --- /dev/null +++ b/jupyter_server/services/sessions/call_context.py @@ -0,0 +1,59 @@ +"""Provides access to variables pertaining to specific call contexts.""" +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +from contextvars import Context, ContextVar, copy_context +from typing import Any, Dict, List + + +class CallContext: + """CallContext essentially acts as a namespace for managing context variables. + + Although not required, it is recommended that any "file-spanning" context variables + (i.e., variables that will be set or retrieved from multiple files or services) be + added to this class definition. + """ + + # Add well-known (service-spanning) names here. + JUPYTER_HANDLER = "JUPYTER_HANDLER" + + # A map of variable name to value is maintained as the single ContextVar. This also enables + # easier management over maintaining a set of ContextVar instances, since the Context is a + # map of ContextVar instances to their values, and the "name" is no longer a lookup key. + _NAME_VALUE_MAP = "_name_value_map" + _name_value_map: ContextVar[Dict[str, Any]] = ContextVar(_NAME_VALUE_MAP) + + @classmethod + def get(cls, name: str) -> Any: + """Returns the value corresponding the named variable relative to this context. + + If the named variable doesn't exist, None will be returned. + """ + name_value_map = CallContext._get_map() + + if name in name_value_map: + return name_value_map[name] + return None # TODO - should this raise `LookupError` (or a custom error derived from said) + + @classmethod + def set(cls, name: str, value: Any) -> None: + """Sets the named variable to the specified value in the current call context.""" + name_value_map = CallContext._get_map() + name_value_map[name] = value + + @classmethod + def context_variable_names(cls) -> List[str]: + """Returns a list of variable names set for this call context.""" + name_value_map = CallContext._get_map() + return list(name_value_map.keys()) + + @classmethod + def _get_map(cls) -> Dict[str, Any]: + """Get the map of names to their values from the _NAME_VALUE_MAP context var. + + If the map does not exist in the current context, an empty map is created and returned. + """ + ctx: Context = copy_context() + if CallContext._name_value_map not in ctx: + CallContext._name_value_map.set({}) + return CallContext._name_value_map.get() diff --git a/tests/test_contextvars.py b/tests/services/sessions/test_call_context.py similarity index 54% rename from tests/test_contextvars.py rename to tests/services/sessions/test_call_context.py index f1690e502d..95350d6704 100644 --- a/tests/test_contextvars.py +++ b/tests/services/sessions/test_call_context.py @@ -2,8 +2,9 @@ import time from jupyter_server.auth.utils import get_anonymous_username -from jupyter_server.base.handlers import CURRENT_JUPYTER_HANDLER, JupyterHandler +from jupyter_server.base.handlers import JupyterHandler from jupyter_server.services.kernels.kernelmanager import AsyncMappingKernelManager +from jupyter_server.services.sessions.call_context import CallContext async def test_jupyter_handler_contextvar(jp_fetch, monkeypatch): @@ -30,11 +31,11 @@ async def get_current_user(self): # not contaminated by other asynchronous parallel requests. def kernel_model(self, kernel_id): # Get the Jupyter Handler from the current context. - current: JupyterHandler = CURRENT_JUPYTER_HANDLER.get() + current: JupyterHandler = CallContext.get(CallContext.JUPYTER_HANDLER) # Get the current user context_tracker[kernel_id]["user"] = current.current_user context_tracker[kernel_id]["started"] = current.current_user - time.sleep(2.0) + time.sleep(1.0) # Track the current user a few seconds later. We'll # verify that this user was unaffected by other parallel # requests. @@ -52,3 +53,52 @@ def kernel_model(self, kernel_id): assert context_tracker[kernel1]["started"] == context_tracker[kernel1]["ended"] # Assert that the second request started+ended with the same user assert context_tracker[kernel2]["started"] == context_tracker[kernel2]["ended"] + + +async def test_context_variable_names(): + CallContext.set("foo", "bar") + CallContext.set("foo2", "bar2") + names = CallContext.context_variable_names() + assert len(names) == 2 + assert set(names) == {"foo", "foo2"} + + +async def test_same_context_operations(): + CallContext.set("foo", "bar") + CallContext.set("foo2", "bar2") + + foo = CallContext.get("foo") + assert foo == "bar" + + CallContext.set("foo", "bar2") + assert CallContext.get("foo") == CallContext.get("foo2") + + +async def test_multi_context_operations(): + async def context1(): + """The "slower" context. This ensures that, following the sleep, the + context variable set prior to the sleep is still the expected value. + If contexts are not managed properly, we should find that context2() has + corrupted context1(). + """ + CallContext.set("foo", "bar1") + await asyncio.sleep(1.0) + assert CallContext.get("foo") == "bar1" + context1_names = CallContext.context_variable_names() + assert len(context1_names) == 1 + + async def context2(): + """The "faster" context. This ensures that CallContext reflects the + appropriate values of THIS context. + """ + CallContext.set("foo", "bar2") + assert CallContext.get("foo") == "bar2" + CallContext.set("foo2", "bar2") + context2_names = CallContext.context_variable_names() + assert len(context2_names) == 2 + + await asyncio.gather(context1(), context2()) + + # Assert that THIS context doesn't have any variables defined. + names = CallContext.context_variable_names() + assert len(names) == 0 From 2bc0f999e32c347c356043be9b012135b00e2e09 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Sat, 8 Apr 2023 09:27:02 -0700 Subject: [PATCH 04/10] Add CallContext to API docs --- .../api/jupyter_server.services.sessions.rst | 6 +++ .../services/sessions/call_context.py | 43 ++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/docs/source/api/jupyter_server.services.sessions.rst b/docs/source/api/jupyter_server.services.sessions.rst index 292de65c34..8c30e71931 100644 --- a/docs/source/api/jupyter_server.services.sessions.rst +++ b/docs/source/api/jupyter_server.services.sessions.rst @@ -16,6 +16,12 @@ Submodules :undoc-members: :show-inheritance: + +.. automodule:: jupyter_server.services.sessions.call_context + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/jupyter_server/services/sessions/call_context.py b/jupyter_server/services/sessions/call_context.py index b643809595..3d989121c2 100644 --- a/jupyter_server/services/sessions/call_context.py +++ b/jupyter_server/services/sessions/call_context.py @@ -9,13 +9,14 @@ class CallContext: """CallContext essentially acts as a namespace for managing context variables. - Although not required, it is recommended that any "file-spanning" context variables - (i.e., variables that will be set or retrieved from multiple files or services) be - added to this class definition. + Although not required, it is recommended that any "file-spanning" context variable + names (i.e., variables that will be set or retrieved from multiple files or services) be + added as constants to this class definition. """ - # Add well-known (service-spanning) names here. - JUPYTER_HANDLER = "JUPYTER_HANDLER" + # Add well-known (file-spanning) names here. + #: Provides access to the current request handler once set. + JUPYTER_HANDLER: str = "JUPYTER_HANDLER" # A map of variable name to value is maintained as the single ContextVar. This also enables # easier management over maintaining a set of ContextVar instances, since the Context is a @@ -28,6 +29,16 @@ def get(cls, name: str) -> Any: """Returns the value corresponding the named variable relative to this context. If the named variable doesn't exist, None will be returned. + + Parameters + ---------- + name : str + The name of the variable to get from the call context + + Returns + ------- + value: Any + The value associated with the named variable for this call context """ name_value_map = CallContext._get_map() @@ -37,13 +48,31 @@ def get(cls, name: str) -> Any: @classmethod def set(cls, name: str, value: Any) -> None: - """Sets the named variable to the specified value in the current call context.""" + """Sets the named variable to the specified value in the current call context. + + Parameters + ---------- + name : str + The name of the variable to store into the call context + value : Any + The value of the variable to store into the call context + + Returns + ------- + None + """ name_value_map = CallContext._get_map() name_value_map[name] = value @classmethod def context_variable_names(cls) -> List[str]: - """Returns a list of variable names set for this call context.""" + """Returns a list of variable names set for this call context. + + Returns + ------- + names: List[str] + A list of variable names set for this call context. + """ name_value_map = CallContext._get_map() return list(name_value_map.keys()) From de66fa1b6f37649920d1604a2d7c581bad435e1e Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Sat, 8 Apr 2023 09:34:38 -0700 Subject: [PATCH 05/10] Alphabetize submodules --- docs/source/api/jupyter_server.services.sessions.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/api/jupyter_server.services.sessions.rst b/docs/source/api/jupyter_server.services.sessions.rst index 8c30e71931..0cf2a8f5bc 100644 --- a/docs/source/api/jupyter_server.services.sessions.rst +++ b/docs/source/api/jupyter_server.services.sessions.rst @@ -5,19 +5,19 @@ Submodules ---------- -.. automodule:: jupyter_server.services.sessions.handlers +.. automodule:: jupyter_server.services.sessions.call_context :members: :undoc-members: :show-inheritance: -.. automodule:: jupyter_server.services.sessions.sessionmanager +.. automodule:: jupyter_server.services.sessions.handlers :members: :undoc-members: :show-inheritance: -.. automodule:: jupyter_server.services.sessions.call_context +.. automodule:: jupyter_server.services.sessions.sessionmanager :members: :undoc-members: :show-inheritance: From 9ee621bf96c24d68e81880c6e26bee015aeaa04b Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Mon, 10 Apr 2023 10:38:51 -0700 Subject: [PATCH 06/10] Unit test contextvar in the kernel shutdown flow --- tests/services/sessions/test_call_context.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/services/sessions/test_call_context.py b/tests/services/sessions/test_call_context.py index 95350d6704..621fab1a6e 100644 --- a/tests/services/sessions/test_call_context.py +++ b/tests/services/sessions/test_call_context.py @@ -26,10 +26,11 @@ async def get_current_user(self): monkeypatch.setattr(JupyterHandler, "get_current_user", get_current_user) - # Monkeypatch the kernel_model method to show that - # the current context variable is truly local and - # not contaminated by other asynchronous parallel requests. - def kernel_model(self, kernel_id): + # Monkeypatch an async method in the mapping kernel manager. + # We chose a method that takes the kernel_id as as required + # first argument to ensure the kernel_id is correct in the + # current context. + async def shutdown_kernel(self, kernel_id, *args, **kwargs): # Get the Jupyter Handler from the current context. current: JupyterHandler = CallContext.get(CallContext.JUPYTER_HANDLER) # Get the current user @@ -40,12 +41,14 @@ def kernel_model(self, kernel_id): # verify that this user was unaffected by other parallel # requests. context_tracker[kernel_id]["ended"] = current.current_user - return {"id": kernel_id, "name": "blah"} - monkeypatch.setattr(AsyncMappingKernelManager, "kernel_model", kernel_model) + monkeypatch.setattr(AsyncMappingKernelManager, "shutdown_kernel", shutdown_kernel) # Make two requests in parallel. - await asyncio.gather(jp_fetch("api", "kernels", kernel1), jp_fetch("api", "kernels", kernel2)) + await asyncio.gather( + jp_fetch("api", "kernels", kernel1, method="DELETE"), + jp_fetch("api", "kernels", kernel2, method="DELETE"), + ) # Assert that the two requests had different users assert context_tracker[kernel1]["user"] != context_tracker[kernel2]["user"] From 0a176593a9e04bc8c529dda48e7117eca3bc93dc Mon Sep 17 00:00:00 2001 From: Zachary Sailer Date: Mon, 10 Apr 2023 11:07:22 -0700 Subject: [PATCH 07/10] Update tests/services/sessions/test_call_context.py Co-authored-by: Kevin Bates --- tests/services/sessions/test_call_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/services/sessions/test_call_context.py b/tests/services/sessions/test_call_context.py index 621fab1a6e..027f3e99a7 100644 --- a/tests/services/sessions/test_call_context.py +++ b/tests/services/sessions/test_call_context.py @@ -36,7 +36,7 @@ async def shutdown_kernel(self, kernel_id, *args, **kwargs): # Get the current user context_tracker[kernel_id]["user"] = current.current_user context_tracker[kernel_id]["started"] = current.current_user - time.sleep(1.0) + await asyncio.sleep(1.0) # Track the current user a few seconds later. We'll # verify that this user was unaffected by other parallel # requests. From b4c6f3f9611a25386de43024f9d5b67442185373 Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Mon, 10 Apr 2023 13:41:28 -0700 Subject: [PATCH 08/10] revert unit test back to using kernel_model --- tests/services/sessions/test_call_context.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/services/sessions/test_call_context.py b/tests/services/sessions/test_call_context.py index 027f3e99a7..abc564921e 100644 --- a/tests/services/sessions/test_call_context.py +++ b/tests/services/sessions/test_call_context.py @@ -1,5 +1,4 @@ import asyncio -import time from jupyter_server.auth.utils import get_anonymous_username from jupyter_server.base.handlers import JupyterHandler @@ -26,11 +25,13 @@ async def get_current_user(self): monkeypatch.setattr(JupyterHandler, "get_current_user", get_current_user) - # Monkeypatch an async method in the mapping kernel manager. - # We chose a method that takes the kernel_id as as required - # first argument to ensure the kernel_id is correct in the - # current context. - async def shutdown_kernel(self, kernel_id, *args, **kwargs): + # Monkeypatch the kernel_model method to show that + # the current context variable is truly local and + # not contaminated by other asynchronous parallel requests. + # Note that even though the current implementation of `kernel_model()` + # is synchronous, we can convert this into an async method because the + # kernel handler wraps the call to `kernel_model()` in `ensure_async()`. + async def kernel_model(self, kernel_id): # Get the Jupyter Handler from the current context. current: JupyterHandler = CallContext.get(CallContext.JUPYTER_HANDLER) # Get the current user @@ -41,13 +42,14 @@ async def shutdown_kernel(self, kernel_id, *args, **kwargs): # verify that this user was unaffected by other parallel # requests. context_tracker[kernel_id]["ended"] = current.current_user + return {"id": kernel_id, "name": "blah"} - monkeypatch.setattr(AsyncMappingKernelManager, "shutdown_kernel", shutdown_kernel) + monkeypatch.setattr(AsyncMappingKernelManager, "kernel_model", kernel_model) # Make two requests in parallel. await asyncio.gather( - jp_fetch("api", "kernels", kernel1, method="DELETE"), - jp_fetch("api", "kernels", kernel2, method="DELETE"), + jp_fetch("api", "kernels", kernel1), + jp_fetch("api", "kernels", kernel2), ) # Assert that the two requests had different users From 70385e011f3e70af1eec89a706a7d0a741f51d1f Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Mon, 10 Apr 2023 15:43:31 -0700 Subject: [PATCH 09/10] Relocate to base package --- jupyter_server/__init__.py | 1 + jupyter_server/{services/sessions => base}/call_context.py | 0 jupyter_server/base/handlers.py | 2 +- tests/{services/sessions => base}/test_call_context.py | 2 +- 4 files changed, 3 insertions(+), 2 deletions(-) rename jupyter_server/{services/sessions => base}/call_context.py (100%) rename tests/{services/sessions => base}/test_call_context.py (98%) diff --git a/jupyter_server/__init__.py b/jupyter_server/__init__.py index c6ca245c91..c0f77cba0a 100644 --- a/jupyter_server/__init__.py +++ b/jupyter_server/__init__.py @@ -15,6 +15,7 @@ del os from ._version import __version__, version_info # noqa +from .base.call_context import CallContext # noqa def _cleanup(): diff --git a/jupyter_server/services/sessions/call_context.py b/jupyter_server/base/call_context.py similarity index 100% rename from jupyter_server/services/sessions/call_context.py rename to jupyter_server/base/call_context.py diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 6862810fdd..061eea672a 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -26,12 +26,12 @@ from traitlets.config import Application import jupyter_server +from jupyter_server import CallContext from jupyter_server._sysinfo import get_sys_info from jupyter_server._tz import utcnow from jupyter_server.auth import authorized from jupyter_server.i18n import combine_translations from jupyter_server.services.security import csp_report_uri -from jupyter_server.services.sessions.call_context import CallContext from jupyter_server.utils import ( ensure_async, filefind, diff --git a/tests/services/sessions/test_call_context.py b/tests/base/test_call_context.py similarity index 98% rename from tests/services/sessions/test_call_context.py rename to tests/base/test_call_context.py index abc564921e..1c12338d61 100644 --- a/tests/services/sessions/test_call_context.py +++ b/tests/base/test_call_context.py @@ -1,9 +1,9 @@ import asyncio +from jupyter_server import CallContext from jupyter_server.auth.utils import get_anonymous_username from jupyter_server.base.handlers import JupyterHandler from jupyter_server.services.kernels.kernelmanager import AsyncMappingKernelManager -from jupyter_server.services.sessions.call_context import CallContext async def test_jupyter_handler_contextvar(jp_fetch, monkeypatch): From ba30c1547588f6f8775ab09b9868718e5332feab Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Mon, 10 Apr 2023 16:02:21 -0700 Subject: [PATCH 10/10] Update location in docs as well --- docs/source/api/jupyter_server.base.rst | 6 ++++++ docs/source/api/jupyter_server.services.sessions.rst | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/api/jupyter_server.base.rst b/docs/source/api/jupyter_server.base.rst index 42aa6211ff..0a8e12c6d1 100644 --- a/docs/source/api/jupyter_server.base.rst +++ b/docs/source/api/jupyter_server.base.rst @@ -5,6 +5,12 @@ Submodules ---------- +.. automodule:: jupyter_server.base.call_context + :members: + :undoc-members: + :show-inheritance: + + .. automodule:: jupyter_server.base.handlers :members: :undoc-members: diff --git a/docs/source/api/jupyter_server.services.sessions.rst b/docs/source/api/jupyter_server.services.sessions.rst index 0cf2a8f5bc..292de65c34 100644 --- a/docs/source/api/jupyter_server.services.sessions.rst +++ b/docs/source/api/jupyter_server.services.sessions.rst @@ -5,12 +5,6 @@ Submodules ---------- -.. automodule:: jupyter_server.services.sessions.call_context - :members: - :undoc-members: - :show-inheritance: - - .. automodule:: jupyter_server.services.sessions.handlers :members: :undoc-members: