| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- """Code for converting notebooks to and from v3."""
- # Copyright (c) IPython Development Team.
- # Distributed under the terms of the Modified BSD License.
- from __future__ import annotations
- import json
- import re
- from traitlets.log import get_logger
- from nbformat import v3, validator
- from nbformat.corpus.words import generate_corpus_id as random_cell_id
- from nbformat.notebooknode import NotebookNode
- from .nbbase import nbformat, nbformat_minor
- def _warn_if_invalid(nb, version):
- """Log validation errors, if there are any."""
- from nbformat import ValidationError, validate
- try:
- validate(nb, version=version)
- except ValidationError as e:
- get_logger().error("Notebook JSON is not valid v%i: %s", version, e)
- def upgrade(nb, from_version=None, from_minor=None):
- """Convert a notebook to latest v4.
- Parameters
- ----------
- nb : NotebookNode
- The Python representation of the notebook to convert.
- from_version : int
- The original version of the notebook to convert.
- from_minor : int
- The original minor version of the notebook to convert (only relevant for v >= 3).
- """
- if not from_version:
- from_version = nb["nbformat"]
- if not from_minor:
- if "nbformat_minor" not in nb:
- if from_version == 4:
- msg = "The v4 notebook does not include the nbformat minor, which is needed."
- raise validator.ValidationError(msg)
- from_minor = 0
- else:
- from_minor = nb["nbformat_minor"]
- if from_version == 3:
- # Validate the notebook before conversion
- _warn_if_invalid(nb, from_version)
- # Mark the original nbformat so consumers know it has been converted
- orig_nbformat = nb.pop("orig_nbformat", None)
- orig_nbformat_minor = nb.pop("orig_nbformat_minor", None)
- nb.metadata.orig_nbformat = orig_nbformat or 3
- nb.metadata.orig_nbformat_minor = orig_nbformat_minor or 0
- # Mark the new format
- nb.nbformat = nbformat
- nb.nbformat_minor = nbformat_minor
- # remove worksheet(s)
- nb["cells"] = cells = []
- # In the unlikely event of multiple worksheets,
- # they will be flattened
- for ws in nb.pop("worksheets", []):
- # upgrade each cell
- for cell in ws["cells"]:
- cells.append(upgrade_cell(cell))
- # upgrade metadata
- nb.metadata.pop("name", "")
- nb.metadata.pop("signature", "")
- # Validate the converted notebook before returning it
- _warn_if_invalid(nb, nbformat)
- return nb
- if from_version == 4:
- if from_minor == nbformat_minor:
- return nb
- # other versions migration code e.g.
- # if from_minor < 3:
- # if from_minor < 4:
- if from_minor < 5:
- for cell in nb.cells:
- cell.id = random_cell_id()
- nb.metadata.orig_nbformat_minor = from_minor
- nb.nbformat_minor = nbformat_minor
- return nb
- raise ValueError(
- "Cannot convert a notebook directly from v%s to v4. "
- "Try using the nbformat.convert module." % from_version
- )
- def upgrade_cell(cell):
- """upgrade a cell from v3 to v4
- heading cell:
- - -> markdown heading
- code cell:
- - remove language metadata
- - cell.input -> cell.source
- - cell.prompt_number -> cell.execution_count
- - update outputs
- """
- cell.setdefault("metadata", NotebookNode())
- cell.id = random_cell_id()
- if cell.cell_type == "code":
- cell.pop("language", "")
- if "collapsed" in cell:
- cell.metadata["collapsed"] = cell.pop("collapsed")
- cell.source = cell.pop("input", "")
- cell.execution_count = cell.pop("prompt_number", None)
- cell.outputs = upgrade_outputs(cell.outputs)
- elif cell.cell_type == "heading":
- cell.cell_type = "markdown"
- level = cell.pop("level", 1)
- cell.source = "{hashes} {single_line}".format(
- hashes="#" * level,
- single_line=" ".join(cell.get("source", "").splitlines()),
- )
- elif cell.cell_type == "html":
- # Technically, this exists. It will never happen in practice.
- cell.cell_type = "markdown"
- return cell
- def downgrade_cell(cell):
- """downgrade a cell from v4 to v3
- code cell:
- - set cell.language
- - cell.input <- cell.source
- - cell.prompt_number <- cell.execution_count
- - update outputs
- markdown cell:
- - single-line heading -> heading cell
- """
- if cell.cell_type == "code":
- cell.language = "python"
- cell.input = cell.pop("source", "")
- cell.prompt_number = cell.pop("execution_count", None)
- cell.collapsed = cell.metadata.pop("collapsed", False)
- cell.outputs = downgrade_outputs(cell.outputs)
- elif cell.cell_type == "markdown":
- source = cell.get("source", "")
- if "\n" not in source and source.startswith("#"):
- match = re.match(r"(#+)\s*(.*)", source)
- assert match is not None
- prefix, text = match.groups()
- cell.cell_type = "heading"
- cell.source = text
- cell.level = len(prefix)
- cell.pop("id", None)
- cell.pop("attachments", None)
- return cell
- _mime_map = {
- "text": "text/plain",
- "html": "text/html",
- "svg": "image/svg+xml",
- "png": "image/png",
- "jpeg": "image/jpeg",
- "latex": "text/latex",
- "json": "application/json",
- "javascript": "application/javascript",
- }
- def to_mime_key(d):
- """convert dict with v3 aliases to plain mime-type keys"""
- for alias, mime in _mime_map.items():
- if alias in d:
- d[mime] = d.pop(alias)
- return d
- def from_mime_key(d):
- """convert dict with mime-type keys to v3 aliases"""
- d2 = {}
- for alias, mime in _mime_map.items():
- if mime in d:
- d2[alias] = d[mime]
- return d2
- def upgrade_output(output):
- """upgrade a single code cell output from v3 to v4
- - pyout -> execute_result
- - pyerr -> error
- - output.type -> output.data.mime/type
- - mime-type keys
- - stream.stream -> stream.name
- """
- if output["output_type"] in {"pyout", "display_data"}:
- output.setdefault("metadata", NotebookNode())
- if output["output_type"] == "pyout":
- output["output_type"] = "execute_result"
- output["execution_count"] = output.pop("prompt_number", None)
- # move output data into data sub-dict
- data = {}
- for key in list(output):
- if key in {"output_type", "execution_count", "metadata"}:
- continue
- data[key] = output.pop(key)
- to_mime_key(data)
- output["data"] = data
- to_mime_key(output.metadata)
- if "application/json" in data:
- data["application/json"] = json.loads(data["application/json"])
- # promote ascii bytes (from v2) to unicode
- for key in ("image/png", "image/jpeg"):
- if key in data and isinstance(data[key], bytes):
- data[key] = data[key].decode("ascii")
- elif output["output_type"] == "pyerr":
- output["output_type"] = "error"
- elif output["output_type"] == "stream":
- output["name"] = output.pop("stream", "stdout")
- return output
- def downgrade_output(output):
- """downgrade a single code cell output to v3 from v4
- - pyout <- execute_result
- - pyerr <- error
- - output.data.mime/type -> output.type
- - un-mime-type keys
- - stream.stream <- stream.name
- """
- if output["output_type"] in {"execute_result", "display_data"}:
- if output["output_type"] == "execute_result":
- output["output_type"] = "pyout"
- output["prompt_number"] = output.pop("execution_count", None)
- # promote data dict to top-level output namespace
- data = output.pop("data", {})
- if "application/json" in data:
- data["application/json"] = json.dumps(data["application/json"])
- data = from_mime_key(data)
- output.update(data)
- from_mime_key(output.get("metadata", {}))
- elif output["output_type"] == "error":
- output["output_type"] = "pyerr"
- elif output["output_type"] == "stream":
- output["stream"] = output.pop("name")
- return output
- def upgrade_outputs(outputs):
- """upgrade outputs of a code cell from v3 to v4"""
- return [upgrade_output(op) for op in outputs]
- def downgrade_outputs(outputs):
- """downgrade outputs of a code cell to v3 from v4"""
- return [downgrade_output(op) for op in outputs]
- def downgrade(nb):
- """Convert a v4 notebook to v3.
- Parameters
- ----------
- nb : NotebookNode
- The Python representation of the notebook to convert.
- """
- if nb.nbformat != nbformat:
- return nb
- # Validate the notebook before conversion
- _warn_if_invalid(nb, nbformat)
- nb.nbformat = v3.nbformat
- nb.nbformat_minor = v3.nbformat_minor
- cells = [downgrade_cell(cell) for cell in nb.pop("cells")]
- nb.worksheets = [v3.new_worksheet(cells=cells)]
- nb.metadata.setdefault("name", "")
- # Validate the converted notebook before returning it
- _warn_if_invalid(nb, v3.nbformat)
- nb.orig_nbformat = nb.metadata.pop("orig_nbformat", nbformat)
- nb.orig_nbformat_minor = nb.metadata.pop("orig_nbformat_minor", nbformat_minor)
- return nb
|