Skip to content

Commit ca3c5b2

Browse files
authored
Increase nbconvert and checkpoints coverage (#1066)
1 parent 44c3742 commit ca3c5b2

File tree

5 files changed

+213
-75
lines changed

5 files changed

+213
-75
lines changed

.github/workflows/python-tests.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ jobs:
3131
uses: actions/checkout@v3
3232
- name: Base Setup
3333
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
34+
- name: Install nbconvert dependencies on Linux
35+
if: startsWith(runner.os, 'Linux')
36+
run: |
37+
sudo apt-get update
38+
sudo apt-get install texlive-plain-generic inkscape texlive-xetex
39+
sudo apt-get install xvfb x11-utils libxkbcommon-x11-0
40+
pip install pandoc
3441
- name: Run the tests
3542
if: ${{ !startsWith(matrix.python-version, 'pypy') && !startsWith(matrix.os, 'windows') }}
3643
run: hatch run cov:test -W default || hatch run cov:test -W default --lf

jupyter_server/services/contents/checkpoints.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,13 @@ class GenericCheckpointsMixin:
7575

7676
def create_checkpoint(self, contents_mgr, path):
7777
model = contents_mgr.get(path, content=True)
78-
type = model["type"]
79-
if type == "notebook":
78+
type_ = model["type"]
79+
if type_ == "notebook":
8080
return self.create_notebook_checkpoint(
8181
model["content"],
8282
path,
8383
)
84-
elif type == "file":
84+
elif type_ == "file":
8585
return self.create_file_checkpoint(
8686
model["content"],
8787
model["format"],
@@ -92,13 +92,13 @@ def create_checkpoint(self, contents_mgr, path):
9292

9393
def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
9494
"""Restore a checkpoint."""
95-
type = contents_mgr.get(path, content=False)["type"]
96-
if type == "notebook":
95+
type_ = contents_mgr.get(path, content=False)["type"]
96+
if type_ == "notebook":
9797
model = self.get_notebook_checkpoint(checkpoint_id, path)
98-
elif type == "file":
98+
elif type_ == "file":
9999
model = self.get_file_checkpoint(checkpoint_id, path)
100100
else:
101-
raise HTTPError(500, "Unexpected type %s" % type)
101+
raise HTTPError(500, "Unexpected type %s" % type_)
102102
contents_mgr.save(model, path)
103103

104104
# Required Methods
@@ -184,30 +184,31 @@ class AsyncGenericCheckpointsMixin(GenericCheckpointsMixin):
184184

185185
async def create_checkpoint(self, contents_mgr, path):
186186
model = await contents_mgr.get(path, content=True)
187-
type = model["type"]
188-
if type == "notebook":
187+
type_ = model["type"]
188+
if type_ == "notebook":
189189
return await self.create_notebook_checkpoint(
190190
model["content"],
191191
path,
192192
)
193-
elif type == "file":
193+
elif type_ == "file":
194194
return await self.create_file_checkpoint(
195195
model["content"],
196196
model["format"],
197197
path,
198198
)
199199
else:
200-
raise HTTPError(500, "Unexpected type %s" % type)
200+
raise HTTPError(500, "Unexpected type %s" % type_)
201201

202202
async def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
203203
"""Restore a checkpoint."""
204-
type = await contents_mgr.get(path, content=False)["type"]
205-
if type == "notebook":
204+
content_model = await contents_mgr.get(path, content=False)
205+
type_ = content_model["type"]
206+
if type_ == "notebook":
206207
model = await self.get_notebook_checkpoint(checkpoint_id, path)
207-
elif type == "file":
208+
elif type_ == "file":
208209
model = await self.get_file_checkpoint(checkpoint_id, path)
209210
else:
210-
raise HTTPError(500, "Unexpected type %s" % type)
211+
raise HTTPError(500, "Unexpected type %s" % type_)
211212
await contents_mgr.save(model, path)
212213

213214
# Required Methods

tests/conftest.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22

33
import pytest
4+
from nbformat import writes
5+
from nbformat.v4 import new_notebook
46

57
from tests.extension.mockextensions.app import MockExtensionApp
68

@@ -84,3 +86,58 @@ def config_file(jp_config_dir):
8486
def jp_mockextension_cleanup():
8587
yield
8688
MockExtensionApp.clear_instance()
89+
90+
91+
@pytest.fixture
92+
def contents_dir(tmp_path, jp_serverapp):
93+
return tmp_path / jp_serverapp.root_dir
94+
95+
96+
dirs = [
97+
("", "inroot"),
98+
("Directory with spaces in", "inspace"),
99+
("unicodé", "innonascii"),
100+
("foo", "a"),
101+
("foo", "b"),
102+
("foo", "name with spaces"),
103+
("foo", "unicodé"),
104+
("foo/bar", "baz"),
105+
("ordering", "A"),
106+
("ordering", "b"),
107+
("ordering", "C"),
108+
("å b", "ç d"),
109+
]
110+
111+
112+
@pytest.fixture
113+
def contents(contents_dir):
114+
# Create files in temporary directory
115+
paths: dict = {"notebooks": [], "textfiles": [], "blobs": [], "contents_dir": contents_dir}
116+
for d, name in dirs:
117+
p = contents_dir / d
118+
p.mkdir(parents=True, exist_ok=True)
119+
120+
# Create a notebook
121+
nb = writes(new_notebook(), version=4)
122+
nbname = p.joinpath(f"{name}.ipynb")
123+
nbname.write_text(nb, encoding="utf-8")
124+
paths["notebooks"].append(nbname.relative_to(contents_dir))
125+
126+
# Create a text file
127+
txt = f"{name} text file"
128+
txtname = p.joinpath(f"{name}.txt")
129+
txtname.write_text(txt, encoding="utf-8")
130+
paths["textfiles"].append(txtname.relative_to(contents_dir))
131+
132+
# Create a random blob
133+
blob = name.encode("utf-8") + b"\xFF"
134+
blobname = p.joinpath(f"{name}.blob")
135+
blobname.write_bytes(blob)
136+
paths["blobs"].append(blobname.relative_to(contents_dir))
137+
paths["all"] = list(paths.values())
138+
return paths
139+
140+
141+
@pytest.fixture
142+
def folders():
143+
return list({item[0] for item in dirs})

tests/services/contents/test_api.py

Lines changed: 3 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88
import pytest
99
import tornado
10-
from nbformat import from_dict, writes
10+
from nbformat import from_dict
1111
from nbformat.v4 import new_markdown_cell, new_notebook
1212

1313
from jupyter_server.utils import url_path_join
14+
from tests.conftest import dirs
1415

1516
from ...utils import expected_http_error
1617

@@ -34,22 +35,6 @@ def dirs_only(dir_model):
3435
return [x for x in dir_model["content"] if x["type"] == "directory"]
3536

3637

37-
dirs = [
38-
("", "inroot"),
39-
("Directory with spaces in", "inspace"),
40-
("unicodé", "innonascii"),
41-
("foo", "a"),
42-
("foo", "b"),
43-
("foo", "name with spaces"),
44-
("foo", "unicodé"),
45-
("foo/bar", "baz"),
46-
("ordering", "A"),
47-
("ordering", "b"),
48-
("ordering", "C"),
49-
("å b", "ç d"),
50-
]
51-
52-
5338
@pytest.fixture(params=["FileContentsManager", "AsyncFileContentsManager"])
5439
def jp_argv(request):
5540
return [
@@ -58,49 +43,6 @@ def jp_argv(request):
5843
]
5944

6045

61-
@pytest.fixture
62-
def contents_dir(tmp_path, jp_serverapp):
63-
return tmp_path / jp_serverapp.root_dir
64-
65-
66-
@pytest.fixture
67-
def contents(contents_dir):
68-
# Create files in temporary directory
69-
paths: dict = {
70-
"notebooks": [],
71-
"textfiles": [],
72-
"blobs": [],
73-
}
74-
for d, name in dirs:
75-
p = contents_dir / d
76-
p.mkdir(parents=True, exist_ok=True)
77-
78-
# Create a notebook
79-
nb = writes(new_notebook(), version=4)
80-
nbname = p.joinpath(f"{name}.ipynb")
81-
nbname.write_text(nb, encoding="utf-8")
82-
paths["notebooks"].append(nbname.relative_to(contents_dir))
83-
84-
# Create a text file
85-
txt = f"{name} text file"
86-
txtname = p.joinpath(f"{name}.txt")
87-
txtname.write_text(txt, encoding="utf-8")
88-
paths["textfiles"].append(txtname.relative_to(contents_dir))
89-
90-
# Create a random blob
91-
blob = name.encode("utf-8") + b"\xFF"
92-
blobname = p.joinpath(f"{name}.blob")
93-
blobname.write_bytes(blob)
94-
paths["blobs"].append(blobname.relative_to(contents_dir))
95-
paths["all"] = list(paths.values())
96-
return paths
97-
98-
99-
@pytest.fixture
100-
def folders():
101-
return list({item[0] for item in dirs})
102-
103-
10446
@pytest.mark.parametrize("path,name", dirs)
10547
async def test_list_notebooks(jp_fetch, contents, path, name):
10648
response = await jp_fetch(
@@ -853,6 +795,7 @@ async def test_rename_400_hidden(jp_fetch, jp_base_url, contents, contents_dir):
853795

854796

855797
async def test_checkpoints_follow_file(jp_fetch, contents):
798+
856799
path = "foo"
857800
name = "a.ipynb"
858801

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import pytest
2+
from jupyter_client.utils import ensure_async
3+
from nbformat import from_dict
4+
from nbformat.v4 import new_markdown_cell
5+
6+
from jupyter_server.services.contents.filecheckpoints import (
7+
AsyncFileCheckpoints,
8+
AsyncGenericFileCheckpoints,
9+
FileCheckpoints,
10+
GenericFileCheckpoints,
11+
)
12+
from jupyter_server.services.contents.largefilemanager import (
13+
AsyncLargeFileManager,
14+
LargeFileManager,
15+
)
16+
17+
param_pairs = [
18+
(LargeFileManager, FileCheckpoints),
19+
(LargeFileManager, GenericFileCheckpoints),
20+
(AsyncLargeFileManager, AsyncFileCheckpoints),
21+
(AsyncLargeFileManager, AsyncGenericFileCheckpoints),
22+
]
23+
24+
25+
@pytest.fixture(params=param_pairs)
26+
def contents_manager(request, contents):
27+
"""Returns a LargeFileManager instance."""
28+
file_manager, checkpoints_class = request.param
29+
root_dir = str(contents["contents_dir"])
30+
return file_manager(root_dir=root_dir, checkpoints_class=checkpoints_class)
31+
32+
33+
async def test_checkpoints_follow_file(contents_manager):
34+
cm: LargeFileManager = contents_manager
35+
path = "foo/a.ipynb"
36+
37+
# Read initial file.
38+
model = await ensure_async(cm.get(path))
39+
40+
# Create a checkpoint of initial state
41+
cp1 = await ensure_async(cm.create_checkpoint(path))
42+
43+
# Modify file and save.
44+
nbcontent = model["content"]
45+
nb = from_dict(nbcontent)
46+
hcell = new_markdown_cell("Created by test")
47+
nb.cells.append(hcell)
48+
nbmodel = {"content": nb, "type": "notebook"}
49+
await ensure_async(cm.save(nbmodel, path))
50+
51+
# List checkpoints
52+
cps = await ensure_async(cm.list_checkpoints(path))
53+
assert cps == [cp1]
54+
55+
model = await ensure_async(cm.get(path))
56+
nbcontent = model["content"]
57+
nb = from_dict(nbcontent)
58+
assert nb.cells[0].source == "Created by test"
59+
60+
61+
async def test_nb_checkpoints(contents_manager):
62+
cm: LargeFileManager = contents_manager
63+
path = "foo/a.ipynb"
64+
model = await ensure_async(cm.get(path))
65+
cp1 = await ensure_async(cm.create_checkpoint(path))
66+
assert set(cp1) == {"id", "last_modified"}
67+
68+
# Modify it.
69+
nbcontent = model["content"]
70+
nb = from_dict(nbcontent)
71+
hcell = new_markdown_cell("Created by test")
72+
nb.cells.append(hcell)
73+
74+
# Save it.
75+
nbmodel = {"content": nb, "type": "notebook"}
76+
await ensure_async(cm.save(nbmodel, path))
77+
78+
# List checkpoints
79+
cps = await ensure_async(cm.list_checkpoints(path))
80+
assert cps == [cp1]
81+
82+
nbcontent = await ensure_async(cm.get(path))
83+
nb = from_dict(nbcontent["content"])
84+
assert nb.cells[0].source == "Created by test"
85+
86+
# Restore Checkpoint cp1
87+
await ensure_async(cm.restore_checkpoint(cp1["id"], path))
88+
89+
nbcontent = await ensure_async(cm.get(path))
90+
nb = from_dict(nbcontent["content"])
91+
assert nb.cells == []
92+
93+
# Delete cp1
94+
await ensure_async(cm.delete_checkpoint(cp1["id"], path))
95+
96+
cps = await ensure_async(cm.list_checkpoints(path))
97+
assert cps == []
98+
99+
100+
async def test_file_checkpoints(contents_manager):
101+
cm: LargeFileManager = contents_manager
102+
path = "foo/a.txt"
103+
model = await ensure_async(cm.get(path))
104+
orig_content = model["content"]
105+
106+
cp1 = await ensure_async(cm.create_checkpoint(path))
107+
assert set(cp1) == {"id", "last_modified"}
108+
109+
# Modify and save it.
110+
model["content"] = new_content = orig_content + "\nsecond line"
111+
await ensure_async(cm.save(model, path))
112+
113+
# List checkpoints
114+
cps = await ensure_async(cm.list_checkpoints(path))
115+
assert cps == [cp1]
116+
117+
model = await ensure_async(cm.get(path))
118+
assert model["content"] == new_content
119+
120+
# Restore Checkpoint cp1
121+
await ensure_async(cm.restore_checkpoint(cp1["id"], path))
122+
123+
restored_content = await ensure_async(cm.get(path))
124+
assert restored_content["content"] == orig_content
125+
126+
# Delete cp1
127+
await ensure_async(cm.delete_checkpoint(cp1["id"], path))
128+
129+
cps = await ensure_async(cm.list_checkpoints(path))
130+
assert cps == []

0 commit comments

Comments
 (0)