Skip to content

Commit 8cd9a5f

Browse files
authored
Merge pull request #5957 from afshin/contents-manager
Allow jupyter_server-based contents managers in notebook
2 parents 5d96514 + ff5399a commit 8cd9a5f

File tree

4 files changed

+486
-18
lines changed

4 files changed

+486
-18
lines changed

notebook/notebookapp.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
urlencode_unix_socket_path,
118118
urljoin,
119119
)
120+
from .traittypes import TypeFromClasses
120121

121122
# Check if we can use async kernel management
122123
try:
@@ -1375,13 +1376,41 @@ def _update_mathjax_config(self, change):
13751376
(shutdown the notebook server)."""
13761377
)
13771378

1378-
contents_manager_class = Type(
1379+
# We relax this trait to handle Contents Managers using jupyter_server
1380+
# as the core backend.
1381+
contents_manager_class = TypeFromClasses(
13791382
default_value=LargeFileManager,
1380-
klass=ContentsManager,
1383+
klasses=[
1384+
ContentsManager,
1385+
# To make custom ContentsManagers both forward+backward
1386+
# compatible, we'll relax the strictness of this trait
1387+
# and allow jupyter_server contents managers to pass
1388+
# through. If jupyter_server is not installed, this class
1389+
# will be ignored.
1390+
'jupyter_server.contents.services.managers.ContentsManager'
1391+
],
13811392
config=True,
13821393
help=_('The notebook manager class to use.')
13831394
)
13841395

1396+
# Throws a deprecation warning to jupyter_server based contents managers.
1397+
@observe('contents_manager_class')
1398+
def _observe_contents_manager_class(self, change):
1399+
new = change['new']
1400+
# If 'new' is a class, get a string representing the import
1401+
# module path.
1402+
if inspect.isclass(new):
1403+
new = new.__module__
1404+
1405+
if new.startswith('jupyter_server'):
1406+
self.log.warning(
1407+
"The specified 'contents_manager_class' class inherits a manager from the "
1408+
"'jupyter_server' package. These (future-looking) managers are not "
1409+
"guaranteed to work with the 'notebook' package. For longer term support "
1410+
"consider switching to NBClassic—a notebook frontend that leverages "
1411+
"Jupyter Server as its server backend."
1412+
)
1413+
13851414
kernel_manager_class = Type(
13861415
default_value=MappingKernelManager,
13871416
klass=MappingKernelManager,

notebook/services/sessions/sessionmanager.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,34 @@
1818
from traitlets import Instance
1919

2020
from notebook.utils import maybe_future
21-
21+
from notebook.traittypes import InstanceFromClasses
2222

2323
class SessionManager(LoggingConfigurable):
2424

2525
kernel_manager = Instance('notebook.services.kernels.kernelmanager.MappingKernelManager')
26-
contents_manager = Instance('notebook.services.contents.manager.ContentsManager')
27-
26+
contents_manager = InstanceFromClasses(
27+
klasses=[
28+
'notebook.services.contents.manager.ContentsManager',
29+
# To make custom ContentsManagers both forward+backward
30+
# compatible, we'll relax the strictness of this trait
31+
# and allow jupyter_server contents managers to pass
32+
# through. If jupyter_server is not installed, this class
33+
# will be ignored.
34+
'jupyter_server.services.contents.manager.ContentsManager'
35+
]
36+
)
37+
2838
# Session database initialized below
2939
_cursor = None
3040
_connection = None
3141
_columns = {'session_id', 'path', 'name', 'type', 'kernel_id'}
32-
42+
3343
@property
3444
def cursor(self):
3545
"""Start a cursor and create a database called 'session'"""
3646
if self._cursor is None:
3747
self._cursor = self.connection.cursor()
38-
self._cursor.execute("""CREATE TABLE session
48+
self._cursor.execute("""CREATE TABLE session
3949
(session_id, path, name, type, kernel_id)""")
4050
return self._cursor
4151

@@ -46,7 +56,7 @@ def connection(self):
4656
self._connection = sqlite3.connect(':memory:')
4757
self._connection.row_factory = sqlite3.Row
4858
return self._connection
49-
59+
5060
def close(self):
5161
"""Close the sqlite connection"""
5262
if self._cursor is not None:
@@ -106,11 +116,11 @@ def start_kernel_for_session(self, session_id, path, name, type, kernel_name):
106116
@gen.coroutine
107117
def save_session(self, session_id, path=None, name=None, type=None, kernel_id=None):
108118
"""Saves the items for the session with the given session_id
109-
119+
110120
Given a session_id (and any other of the arguments), this method
111121
creates a row in the sqlite session database that holds the information
112122
for a session.
113-
123+
114124
Parameters
115125
----------
116126
session_id : str
@@ -123,7 +133,7 @@ def save_session(self, session_id, path=None, name=None, type=None, kernel_id=No
123133
the type of the session
124134
kernel_id : str
125135
a uuid for the kernel associated with this session
126-
136+
127137
Returns
128138
-------
129139
model : dict
@@ -138,7 +148,7 @@ def save_session(self, session_id, path=None, name=None, type=None, kernel_id=No
138148
@gen.coroutine
139149
def get_session(self, **kwargs):
140150
"""Returns the model for a particular session.
141-
151+
142152
Takes a keyword argument and searches for the value in the session
143153
database, then returns the rest of the session's info.
144154
@@ -151,7 +161,7 @@ def get_session(self, **kwargs):
151161
Returns
152162
-------
153163
model : dict
154-
returns a dictionary that includes all the information from the
164+
returns a dictionary that includes all the information from the
155165
session described by the kwarg.
156166
"""
157167
if not kwargs:
@@ -185,17 +195,17 @@ def get_session(self, **kwargs):
185195
@gen.coroutine
186196
def update_session(self, session_id, **kwargs):
187197
"""Updates the values in the session database.
188-
198+
189199
Changes the values of the session with the given session_id
190-
with the values from the keyword arguments.
191-
200+
with the values from the keyword arguments.
201+
192202
Parameters
193203
----------
194204
session_id : str
195205
a uuid that identifies a session in the sqlite3 database
196206
**kwargs : str
197207
the key must correspond to a column title in session database,
198-
and the value replaces the current value in the session
208+
and the value replaces the current value in the session
199209
with session_id.
200210
"""
201211
yield maybe_future(self.get_session(session_id=session_id))
@@ -228,7 +238,7 @@ def row_to_model(self, row, tolerate_culled=False):
228238
# If caller wishes to tolerate culled kernels, log a warning
229239
# and return None. Otherwise, raise KeyError with a similar
230240
# message.
231-
self.cursor.execute("DELETE FROM session WHERE session_id=?",
241+
self.cursor.execute("DELETE FROM session WHERE session_id=?",
232242
(row['session_id'],))
233243
msg = "Kernel '{kernel_id}' appears to have been culled or died unexpectedly, " \
234244
"invalidating session '{session_id}'. The session has been removed.".\

notebook/tests/test_traittypes.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import pytest
2+
from traitlets import HasTraits, TraitError
3+
from traitlets.utils.importstring import import_item
4+
5+
from notebook.traittypes import (
6+
InstanceFromClasses,
7+
TypeFromClasses
8+
)
9+
from notebook.services.contents.largefilemanager import LargeFileManager
10+
11+
12+
class DummyClass:
13+
"""Dummy class for testing Instance"""
14+
15+
16+
class DummyInt(int):
17+
"""Dummy class for testing types."""
18+
19+
20+
class Thing(HasTraits):
21+
22+
a = InstanceFromClasses(
23+
default_value=2,
24+
klasses=[
25+
int,
26+
str,
27+
DummyClass,
28+
]
29+
)
30+
31+
b = TypeFromClasses(
32+
default_value=None,
33+
allow_none=True,
34+
klasses=[
35+
DummyClass,
36+
int,
37+
'notebook.services.contents.manager.ContentsManager'
38+
]
39+
)
40+
41+
42+
class TestInstanceFromClasses:
43+
44+
@pytest.mark.parametrize(
45+
'value',
46+
[1, 'test', DummyClass()]
47+
)
48+
def test_good_values(self, value):
49+
thing = Thing(a=value)
50+
assert thing.a == value
51+
52+
@pytest.mark.parametrize(
53+
'value',
54+
[2.4, object()]
55+
)
56+
def test_bad_values(self, value):
57+
with pytest.raises(TraitError) as e:
58+
thing = Thing(a=value)
59+
60+
61+
class TestTypeFromClasses:
62+
63+
@pytest.mark.parametrize(
64+
'value',
65+
[DummyClass, DummyInt, LargeFileManager,
66+
'notebook.services.contents.manager.ContentsManager']
67+
)
68+
def test_good_values(self, value):
69+
thing = Thing(b=value)
70+
if isinstance(value, str):
71+
value = import_item(value)
72+
assert thing.b == value
73+
74+
@pytest.mark.parametrize(
75+
'value',
76+
[float, object]
77+
)
78+
def test_bad_values(self, value):
79+
with pytest.raises(TraitError) as e:
80+
thing = Thing(b=value)

0 commit comments

Comments
 (0)