Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NBectomy #891

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions jupyter_server/serverapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
from jupyter_client.session import Session
from jupyter_core.application import JupyterApp, base_aliases, base_flags
from jupyter_core.paths import jupyter_runtime_dir
from nbformat.sign import NotebookNotary
from traitlets import (
Any,
Bool,
Expand Down Expand Up @@ -769,7 +768,6 @@ class ServerApp(JupyterApp):
FileContentsManager,
AsyncContentsManager,
AsyncFileContentsManager,
NotebookNotary,
GatewayMappingKernelManager,
GatewayKernelSpecManager,
GatewaySessionManager,
Expand Down
111 changes: 15 additions & 96 deletions jupyter_server/services/contents/fileio.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import errno
import json
import os
import shutil
from base64 import decodebytes, encodebytes
from contextlib import contextmanager
from functools import partial

import nbformat
from anyio.to_thread import run_sync
from tornado.web import HTTPError
from traitlets import Bool
Expand Down Expand Up @@ -259,47 +258,8 @@ def _get_os_path(self, path):
raise HTTPError(404, "%s is outside root contents directory" % path)
return os_path

def _read_notebook(self, os_path, as_version=4, capture_validation_error=None):
"""Read a notebook from an os path."""
with self.open(os_path, "r", encoding="utf-8") as f:
try:
return nbformat.read(
f, as_version=as_version, capture_validation_error=capture_validation_error
)
except Exception as e:
e_orig = e

# If use_atomic_writing is enabled, we'll guess that it was also
# enabled when this notebook was written and look for a valid
# atomic intermediate.
tmp_path = path_to_intermediate(os_path)

if not self.use_atomic_writing or not os.path.exists(tmp_path):
raise HTTPError(
400,
f"Unreadable Notebook: {os_path} {e_orig!r}",
)

# Move the bad file aside, restore the intermediate, and try again.
invalid_file = path_to_invalid(os_path)
replace_file(os_path, invalid_file)
replace_file(tmp_path, os_path)
return self._read_notebook(
os_path, as_version, capture_validation_error=capture_validation_error
)

def _save_notebook(self, os_path, nb, capture_validation_error=None):
"""Save a notebook to an os_path."""
with self.atomic_writing(os_path, encoding="utf-8") as f:
nbformat.write(
nb,
f,
version=nbformat.NO_CONVERT,
capture_validation_error=capture_validation_error,
)

def _read_file(self, os_path, format):
"""Read a non-notebook file.
"""Read a file.

os_path: The path to be read.
format:
Expand Down Expand Up @@ -329,13 +289,16 @@ def _read_file(self, os_path, format):

def _save_file(self, os_path, content, format):
"""Save content of a generic file."""
if format not in {"text", "base64"}:
format = format or "text"
if format not in {"json", "text", "base64"}:
raise HTTPError(
400,
"Must specify format of file contents as 'text' or 'base64'",
"Must specify format of file contents as 'json', 'text' or 'base64'",
)
try:
if format == "text":
if format == "json":
bcontent = json.dumps(content).encode("utf8")
elif format == "text":
bcontent = content.encode("utf8")
else:
b64_bytes = content.encode("ascii")
Expand All @@ -359,55 +322,8 @@ async def _copy(self, src, dest):
"""
await async_copy2_safe(src, dest, log=self.log)

async def _read_notebook(self, os_path, as_version=4, capture_validation_error=None):
"""Read a notebook from an os path."""
with self.open(os_path, "r", encoding="utf-8") as f:
try:
return await run_sync(
partial(
nbformat.read,
as_version=as_version,
capture_validation_error=capture_validation_error,
),
f,
)
except Exception as e:
e_orig = e

# If use_atomic_writing is enabled, we'll guess that it was also
# enabled when this notebook was written and look for a valid
# atomic intermediate.
tmp_path = path_to_intermediate(os_path)

if not self.use_atomic_writing or not os.path.exists(tmp_path):
raise HTTPError(
400,
f"Unreadable Notebook: {os_path} {e_orig!r}",
)

# Move the bad file aside, restore the intermediate, and try again.
invalid_file = path_to_invalid(os_path)
await async_replace_file(os_path, invalid_file)
await async_replace_file(tmp_path, os_path)
return await self._read_notebook(
os_path, as_version, capture_validation_error=capture_validation_error
)

async def _save_notebook(self, os_path, nb, capture_validation_error=None):
"""Save a notebook to an os_path."""
with self.atomic_writing(os_path, encoding="utf-8") as f:
await run_sync(
partial(
nbformat.write,
version=nbformat.NO_CONVERT,
capture_validation_error=capture_validation_error,
),
nb,
f,
)

async def _read_file(self, os_path, format):
"""Read a non-notebook file.
"""Read a file.

os_path: The path to be read.
format:
Expand Down Expand Up @@ -437,13 +353,16 @@ async def _read_file(self, os_path, format):

async def _save_file(self, os_path, content, format):
"""Save content of a generic file."""
if format not in {"text", "base64"}:
format = format or "text"
if format not in {"json", "text", "base64"}:
raise HTTPError(
400,
"Must specify format of file contents as 'text' or 'base64'",
"Must specify format of file contents as 'json', 'text' or 'base64'",
)
try:
if format == "text":
if format == "json":
bcontent = json.dumps(content).encode("utf8")
elif format == "text":
bcontent = content.encode("utf8")
else:
b64_bytes = content.encode("ascii")
Expand Down
99 changes: 10 additions & 89 deletions jupyter_server/services/contents/filemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import sys
from datetime import datetime

import nbformat
from anyio.to_thread import run_sync
from jupyter_core.paths import exists, is_file_hidden, is_hidden
from send2trash import send2trash
Expand Down Expand Up @@ -326,28 +325,6 @@ def _file_model(self, path, content=True, format=None):

return model

def _notebook_model(self, path, content=True):
"""Build a notebook model

if content is requested, the notebook content will be populated
as a JSON structure (not double-serialized)
"""
model = self._base_model(path)
model["type"] = "notebook"
os_path = self._get_os_path(path)

if content:
validation_error: dict = {}
nb = self._read_notebook(
os_path, as_version=4, capture_validation_error=validation_error
)
self.mark_trusted_cells(nb, path)
model["content"] = nb
model["format"] = "json"
self.validate_notebook_model(model, validation_error)

return model

def get(self, path, content=True, type=None, format=None):
"""Takes a path for an entity and returns its model

Expand All @@ -358,11 +335,11 @@ def get(self, path, content=True, type=None, format=None):
content : bool
Whether to include the contents in the reply
type : str, optional
The requested type - 'file', 'notebook', or 'directory'.
The requested type - 'file' or 'directory'.
Will raise HTTPError 400 if the content doesn't match.
format : str, optional
The requested format for file contents. 'text' or 'base64'.
Ignored if this returns a notebook or directory model.
Ignored if this returns a directory model.

Returns
-------
Expand All @@ -389,8 +366,6 @@ def get(self, path, content=True, type=None, format=None):
reason="bad type",
)
model = self._dir_model(path, content=content)
elif type == "notebook" or (type is None and path.endswith(".ipynb")):
model = self._notebook_model(path, content=content)
else:
if type == "directory":
raise web.HTTPError(400, "%s is not a directory" % path, reason="bad type")
Expand Down Expand Up @@ -427,20 +402,12 @@ def save(self, model, path=""):

self.log.debug("Saving %s", os_path)

validation_error: dict = {}
try:
if model["type"] == "notebook":
nb = nbformat.from_dict(model["content"])
self.check_and_sign(nb, path)
self._save_notebook(os_path, nb, capture_validation_error=validation_error)
# One checkpoint should always exist for notebooks.
if not self.checkpoints.list_checkpoints(path):
self.create_checkpoint(path)
elif model["type"] == "file":
if model["type"] == "directory":
self._save_directory(os_path, model, path)
elif model["type"] in ("file", "notebook"): # keep notebook for backwards compatibility
# Missing format will be handled internally by _save_file.
self._save_file(os_path, model["content"], model.get("format"))
elif model["type"] == "directory":
self._save_directory(os_path, model, path)
else:
raise web.HTTPError(400, "Unhandled contents type: %s" % model["type"])
except web.HTTPError:
Expand All @@ -449,14 +416,7 @@ def save(self, model, path=""):
self.log.error("Error while saving file: %s %s", path, e, exc_info=True)
raise web.HTTPError(500, f"Unexpected error while saving file: {path} {e}") from e

validation_message = None
if model["type"] == "notebook":
self.validate_notebook_model(model, validation_error=validation_error)
validation_message = model.get("message", None)

model = self.get(path, content=False)
if validation_message:
model["message"] = validation_message

self.run_post_save_hooks(model=model, os_path=os_path)

Expand Down Expand Up @@ -671,28 +631,6 @@ async def _file_model(self, path, content=True, format=None):

return model

async def _notebook_model(self, path, content=True):
"""Build a notebook model

if content is requested, the notebook content will be populated
as a JSON structure (not double-serialized)
"""
model = self._base_model(path)
model["type"] = "notebook"
os_path = self._get_os_path(path)

if content:
validation_error: dict = {}
nb = await self._read_notebook(
os_path, as_version=4, capture_validation_error=validation_error
)
self.mark_trusted_cells(nb, path)
model["content"] = nb
model["format"] = "json"
self.validate_notebook_model(model, validation_error)

return model

async def get(self, path, content=True, type=None, format=None):
"""Takes a path for an entity and returns its model

Expand All @@ -703,11 +641,11 @@ async def get(self, path, content=True, type=None, format=None):
content : bool
Whether to include the contents in the reply
type : str, optional
The requested type - 'file', 'notebook', or 'directory'.
The requested type - 'file' or 'directory'.
Will raise HTTPError 400 if the content doesn't match.
format : str, optional
The requested format for file contents. 'text' or 'base64'.
Ignored if this returns a notebook or directory model.
Ignored if this returns directory model.

Returns
-------
Expand All @@ -729,8 +667,6 @@ async def get(self, path, content=True, type=None, format=None):
reason="bad type",
)
model = await self._dir_model(path, content=content)
elif type == "notebook" or (type is None and path.endswith(".ipynb")):
model = await self._notebook_model(path, content=content)
else:
if type == "directory":
raise web.HTTPError(400, "%s is not a directory" % path, reason="bad type")
Expand Down Expand Up @@ -763,20 +699,12 @@ async def save(self, model, path=""):
os_path = self._get_os_path(path)
self.log.debug("Saving %s", os_path)

validation_error: dict = {}
try:
if model["type"] == "notebook":
nb = nbformat.from_dict(model["content"])
self.check_and_sign(nb, path)
await self._save_notebook(os_path, nb, capture_validation_error=validation_error)
# One checkpoint should always exist for notebooks.
if not (await self.checkpoints.list_checkpoints(path)):
await self.create_checkpoint(path)
elif model["type"] == "file":
if model["type"] == "directory":
await self._save_directory(os_path, model, path)
elif model["type"] in ("file", "notebook"): # keep notebook for backwards compatibility
# Missing format will be handled internally by _save_file.
await self._save_file(os_path, model["content"], model.get("format"))
elif model["type"] == "directory":
await self._save_directory(os_path, model, path)
else:
raise web.HTTPError(400, "Unhandled contents type: %s" % model["type"])
except web.HTTPError:
Expand All @@ -785,14 +713,7 @@ async def save(self, model, path=""):
self.log.error("Error while saving file: %s %s", path, e, exc_info=True)
raise web.HTTPError(500, f"Unexpected error while saving file: {path} {e}") from e

validation_message = None
if model["type"] == "notebook":
self.validate_notebook_model(model, validation_error=validation_error)
validation_message = model.get("message", None)

model = await self.get(path, content=False)
if validation_message:
model["message"] = validation_message

self.run_post_save_hooks(model=model, os_path=os_path)

Expand Down
4 changes: 3 additions & 1 deletion jupyter_server/services/contents/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ async def get(self, path=""):
cm = self.contents_manager

type = self.get_query_argument("type", default=None)
if type not in {None, "directory", "file", "notebook"}:
if type == "notebook":
type = "file"
if type not in {None, "directory", "file"}:
raise web.HTTPError(400, "Type %r is invalid" % type)

format = self.get_query_argument("format", default=None)
Expand Down
Loading