Skip to content

Commit 3d516b3

Browse files
Carreaukevin-bates
andauthored
Set JPY_SESSION_NAME to full notebook path. (#1100)
* Set JPY_SESSION_NAME to full notebook path. This also add some typing here and there, and extend one of the console warning to log an exception when there is an error. My main concern is that get_kernel_env need to become async. Co-authored-by: Kevin Bates <[email protected]> * fix linter/typing * workaround type ignore * fix a few lints Co-authored-by: Kevin Bates <[email protected]>
1 parent 8ca5f5e commit 3d516b3

File tree

9 files changed

+123
-43
lines changed

9 files changed

+123
-43
lines changed

jupyter_server/base/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ def write_error(self, status_code, **kwargs):
735735
reply["message"] = "Unhandled error"
736736
reply["reason"] = None
737737
reply["traceback"] = "".join(traceback.format_exception(*exc_info))
738-
self.log.warning("wrote error: %r", reply["message"])
738+
self.log.warning("wrote error: %r", reply["message"], exc_info=True)
739739
self.finish(json.dumps(reply))
740740

741741
def get_login_url(self):

jupyter_server/gateway/managers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def remove_kernel(self, kernel_id):
5757
except KeyError:
5858
pass
5959

60-
async def start_kernel(self, kernel_id=None, path=None, **kwargs):
60+
async def start_kernel(self, *, kernel_id=None, path=None, **kwargs):
6161
"""Start a kernel for a session and return its kernel_id.
6262
6363
Parameters
@@ -323,7 +323,7 @@ class GatewaySessionManager(SessionManager):
323323

324324
kernel_manager = Instance("jupyter_server.gateway.managers.GatewayMappingKernelManager")
325325

326-
async def kernel_culled(self, kernel_id: str) -> bool:
326+
async def kernel_culled(self, kernel_id: str) -> bool: # typing: ignore
327327
"""Checks if the kernel is still considered alive and returns true if it's not found."""
328328
km: Optional[GatewayKernelManager] = None
329329
try:

jupyter_server/services/contents/fileio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from traitlets import Bool
1717
from traitlets.config import Configurable
1818

19-
from jupyter_server.utils import to_api_path, to_os_path
19+
from jupyter_server.utils import ApiPath, to_api_path, to_os_path
2020

2121

2222
def replace_file(src, dst):
@@ -257,7 +257,7 @@ def _get_os_path(self, path):
257257
# to_os_path is not safe if path starts with a drive, since os.path.join discards first part
258258
if os.path.splitdrive(path)[0]:
259259
raise HTTPError(404, "%s is not a relative API path" % path)
260-
os_path = to_os_path(path, root)
260+
os_path = to_os_path(ApiPath(path), root)
261261
if not (os.path.abspath(os_path) + os.path.sep).startswith(root):
262262
raise HTTPError(404, "%s is outside root contents directory" % path)
263263
return os_path

jupyter_server/services/kernels/kernelmanager.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
# Distributed under the terms of the Modified BSD License.
88
import asyncio
99
import os
10+
import typing as t
1011
import warnings
1112
from collections import defaultdict
1213
from datetime import datetime, timedelta
1314
from functools import partial
15+
from typing import Dict as DictType
16+
from typing import Optional
1417

1518
from jupyter_client.ioloop.manager import AsyncIOLoopKernelManager
1619
from jupyter_client.multikernelmanager import AsyncMultiKernelManager, MultiKernelManager
@@ -36,7 +39,7 @@
3639

3740
from jupyter_server._tz import isoformat, utcnow
3841
from jupyter_server.prometheus.metrics import KERNEL_CURRENTLY_RUNNING_TOTAL
39-
from jupyter_server.utils import import_item, to_os_path
42+
from jupyter_server.utils import ApiPath, import_item, to_os_path
4043

4144

4245
class MappingKernelManager(MultiKernelManager):
@@ -56,7 +59,7 @@ def _default_kernel_manager_class(self):
5659

5760
_kernel_connections = Dict()
5861

59-
_kernel_ports = Dict()
62+
_kernel_ports: DictType[str, t.List[int]] = Dict() # type: ignore
6063

6164
_culler_callback = None
6265

@@ -196,12 +199,16 @@ async def _remove_kernel_when_ready(self, kernel_id, kernel_awaitable):
196199
self._kernel_connections.pop(kernel_id, None)
197200
self._kernel_ports.pop(kernel_id, None)
198201

199-
async def _async_start_kernel(self, kernel_id=None, path=None, **kwargs):
202+
# TODO DEC 2022: Revise the type-ignore once the signatures have been changed upstream
203+
# https://github.com/jupyter/jupyter_client/pull/905
204+
async def _async_start_kernel( # type:ignore[override]
205+
self, *, kernel_id: Optional[str] = None, path: Optional[ApiPath] = None, **kwargs: str
206+
) -> str:
200207
"""Start a kernel for a session and return its kernel_id.
201208
202209
Parameters
203210
----------
204-
kernel_id : uuid
211+
kernel_id : uuid (str)
205212
The uuid to associate the new kernel with. If this
206213
is not None, this kernel will be persistent whenever it is
207214
requested.
@@ -216,6 +223,7 @@ async def _async_start_kernel(self, kernel_id=None, path=None, **kwargs):
216223
if path is not None:
217224
kwargs["cwd"] = self.cwd_for_path(path, env=kwargs.get("env", {}))
218225
if kernel_id is not None:
226+
assert kernel_id is not None, "Never Fail, but necessary for mypy "
219227
kwargs["kernel_id"] = kernel_id
220228
kernel_id = await self.pinned_superclass._async_start_kernel(self, **kwargs)
221229
self._kernel_connections[kernel_id] = 0
@@ -242,9 +250,12 @@ async def _async_start_kernel(self, kernel_id=None, path=None, **kwargs):
242250
# Initialize culling if not already
243251
if not self._initialized_culler:
244252
self.initialize_culler()
245-
253+
assert kernel_id is not None
246254
return kernel_id
247255

256+
# see https://github.com/jupyter-server/jupyter_server/issues/1165
257+
# this assignment is technically incorrect, but might need a change of API
258+
# in jupyter_client.
248259
start_kernel = _async_start_kernel
249260

250261
async def _finish_kernel_start(self, kernel_id):
@@ -299,6 +310,8 @@ def _get_changed_ports(self, kernel_id):
299310
"""
300311
# Get current ports and return comparison with ports captured at startup.
301312
km = self.get_kernel(kernel_id)
313+
assert isinstance(km.ports, list)
314+
assert isinstance(self._kernel_ports[kernel_id], list)
302315
if km.ports != self._kernel_ports[kernel_id]:
303316
return km.ports
304317
return None

jupyter_server/services/sessions/handlers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,17 @@ async def post(self):
5252
if model is None:
5353
raise web.HTTPError(400, "No JSON data provided")
5454

55-
if "notebook" in model and "path" in model["notebook"]:
55+
if "notebook" in model:
5656
self.log.warning("Sessions API changed, see updated swagger docs")
57-
model["path"] = model["notebook"]["path"]
5857
model["type"] = "notebook"
58+
if "name" in model["notebook"]:
59+
model["path"] = model["notebook"]["name"]
60+
elif "path" in model["notebook"]:
61+
model["path"] = model["notebook"]["path"]
5962

6063
try:
64+
# There is a high chance here that `path` is not a path but
65+
# a unique session id
6166
path = model["path"]
6267
except KeyError as e:
6368
raise web.HTTPError(400, "Missing field in JSON data: path") from e

jupyter_server/services/sessions/sessionmanager.py

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
"""A base class session manager."""
2+
23
# Copyright (c) Jupyter Development Team.
34
# Distributed under the terms of the Modified BSD License.
45
import os
56
import pathlib
67
import uuid
8+
from typing import Any, Dict, List, NewType, Optional, Union
9+
10+
KernelName = NewType("KernelName", str)
11+
ModelName = NewType("ModelName", str)
712

813
try:
914
import sqlite3
@@ -12,7 +17,6 @@
1217
from pysqlite2 import dbapi2 as sqlite3 # type:ignore[no-redef]
1318

1419
from dataclasses import dataclass, fields
15-
from typing import Union
1620

1721
from jupyter_core.utils import ensure_async
1822
from tornado import web
@@ -39,8 +43,8 @@ class KernelSessionRecord:
3943
associated with them.
4044
"""
4145

42-
session_id: Union[None, str] = None
43-
kernel_id: Union[None, str] = None
46+
session_id: Optional[str] = None
47+
kernel_id: Optional[str] = None
4448

4549
def __eq__(self, other: object) -> bool:
4650
"""Whether a record equals another."""
@@ -98,7 +102,9 @@ class KernelSessionRecordList:
98102
it will be appended.
99103
"""
100104

101-
def __init__(self, *records):
105+
_records: List[KernelSessionRecord]
106+
107+
def __init__(self, *records: KernelSessionRecord):
102108
"""Initialize a record list."""
103109
self._records = []
104110
for record in records:
@@ -252,14 +258,26 @@ async def session_exists(self, path):
252258
exists = True
253259
return exists
254260

255-
def new_session_id(self):
261+
def new_session_id(self) -> str:
256262
"""Create a uuid for a new session"""
257263
return str(uuid.uuid4())
258264

259265
async def create_session(
260-
self, path=None, name=None, type=None, kernel_name=None, kernel_id=None
261-
):
262-
"""Creates a session and returns its model"""
266+
self,
267+
path: Optional[str] = None,
268+
name: Optional[ModelName] = None,
269+
type: Optional[str] = None,
270+
kernel_name: Optional[KernelName] = None,
271+
kernel_id: Optional[str] = None,
272+
) -> Dict[str, Any]:
273+
"""Creates a session and returns its model
274+
275+
name: ModelName(str)
276+
Usually the model name, like the filename associated with current
277+
kernel.
278+
279+
280+
"""
263281
session_id = self.new_session_id()
264282
record = KernelSessionRecord(session_id=session_id)
265283
self._pending_sessions.update(record)
@@ -277,15 +295,52 @@ async def create_session(
277295
self._pending_sessions.remove(record)
278296
return result
279297

280-
def get_kernel_env(self, path):
281-
"""Return the environment variables that need to be set in the kernel"""
298+
def get_kernel_env(
299+
self, path: Optional[str], name: Optional[ModelName] = None
300+
) -> Dict[str, str]:
301+
"""Return the environment variables that need to be set in the kernel
302+
303+
path : str
304+
the url path for the given session.
305+
name: ModelName(str), optional
306+
Here the name is likely to be the name of the associated file
307+
with the current kernel at startup time.
308+
309+
310+
"""
311+
if name is not None:
312+
cwd = self.kernel_manager.cwd_for_path(path)
313+
path = os.path.join(cwd, name)
314+
assert isinstance(path, str)
282315
return {**os.environ, "JPY_SESSION_NAME": path}
283316

284-
async def start_kernel_for_session(self, session_id, path, name, type, kernel_name):
285-
"""Start a new kernel for a given session."""
317+
async def start_kernel_for_session(
318+
self,
319+
session_id: str,
320+
path: Optional[str],
321+
name: Optional[ModelName],
322+
type: Optional[str],
323+
kernel_name: Optional[KernelName],
324+
) -> str:
325+
"""Start a new kernel for a given session.
326+
327+
session_id : str
328+
uuid for the session; this method must be given a session_id
329+
path : str
330+
the path for the given session - seem to be a session id sometime.
331+
name : str
332+
Usually the model name, like the filename associated with current
333+
kernel.
334+
type : str
335+
the type of the session
336+
kernel_name : str
337+
the name of the kernel specification to use. The default kernel name will be used if not provided.
338+
339+
"""
286340
# allow contents manager to specify kernels cwd
287341
kernel_path = await ensure_async(self.contents_manager.get_kernel_path(path=path))
288-
kernel_env = self.get_kernel_env(path)
342+
343+
kernel_env = self.get_kernel_env(path, name)
289344
kernel_id = await self.kernel_manager.start_kernel(
290345
path=kernel_path,
291346
kernel_name=kernel_name,
@@ -306,9 +361,9 @@ async def save_session(self, session_id, path=None, name=None, type=None, kernel
306361
uuid for the session; this method must be given a session_id
307362
path : str
308363
the path for the given session
309-
name: str
364+
name : str
310365
the name of the session
311-
type: string
366+
type : str
312367
the type of the session
313368
kernel_id : str
314369
a uuid for the kernel associated with this session
@@ -405,13 +460,13 @@ async def update_session(self, session_id, **kwargs):
405460
query = "UPDATE session SET %s WHERE session_id=?" % (", ".join(sets))
406461
self.cursor.execute(query, list(kwargs.values()) + [session_id])
407462

408-
def kernel_culled(self, kernel_id):
463+
async def kernel_culled(self, kernel_id: str) -> bool:
409464
"""Checks if the kernel is still considered alive and returns true if its not found."""
410465
return kernel_id not in self.kernel_manager
411466

412467
async def row_to_model(self, row, tolerate_culled=False):
413468
"""Takes sqlite database session row and turns it into a dictionary"""
414-
kernel_culled = await ensure_async(self.kernel_culled(row["kernel_id"]))
469+
kernel_culled: bool = await ensure_async(self.kernel_culled(row["kernel_id"]))
415470
if kernel_culled:
416471
# The kernel was culled or died without deleting the session.
417472
# We can't use delete_session here because that tries to find

jupyter_server/utils.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99
import warnings
1010
from contextlib import contextmanager
11+
from typing import NewType
1112
from urllib.parse import (
1213
SplitResult,
1314
quote,
@@ -17,14 +18,16 @@
1718
urlsplit,
1819
urlunsplit,
1920
)
20-
from urllib.request import pathname2url # noqa
21+
from urllib.request import pathname2url # noqa: F401
2122

2223
from _frozen_importlib_external import _NamespacePath
2324
from jupyter_core.utils import ensure_async
2425
from packaging.version import Version
2526
from tornado.httpclient import AsyncHTTPClient, HTTPClient, HTTPRequest
2627
from tornado.netutil import Resolver
2728

29+
ApiPath = NewType("ApiPath", str)
30+
2831

2932
def url_path_join(*pieces):
3033
"""Join components of url into a relative url
@@ -109,19 +112,19 @@ def samefile_simple(path, other_path):
109112
return path.lower() == other_path.lower() and path_stat == other_path_stat
110113

111114

112-
def to_os_path(path, root=""):
115+
def to_os_path(path: ApiPath, root: str = "") -> str:
113116
"""Convert an API path to a filesystem path
114117
115118
If given, root will be prepended to the path.
116119
root must be a filesystem path already.
117120
"""
118-
parts = path.strip("/").split("/")
121+
parts = str(path).strip("/").split("/")
119122
parts = [p for p in parts if p != ""] # remove duplicate splits
120-
path = os.path.join(root, *parts)
121-
return os.path.normpath(path)
123+
path_ = os.path.join(root, *parts)
124+
return os.path.normpath(path_)
122125

123126

124-
def to_api_path(os_path, root=""):
127+
def to_api_path(os_path: str, root: str = "") -> ApiPath:
125128
"""Convert a filesystem path to an API path
126129
127130
If given, root will be removed from the path.
@@ -132,7 +135,7 @@ def to_api_path(os_path, root=""):
132135
parts = os_path.strip(os.path.sep).split(os.path.sep)
133136
parts = [p for p in parts if p != ""] # remove duplicate splits
134137
path = "/".join(parts)
135-
return path
138+
return ApiPath(path)
136139

137140

138141
def check_version(v, check):

0 commit comments

Comments
 (0)