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

Version conversion, support for X to Y even if Y < X (nbformat) #4198

Merged
merged 12 commits into from
Oct 29, 2013
72 changes: 72 additions & 0 deletions IPython/nbformat/convert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""API for converting notebooks between versions.

Authors:

* Jonathan Frederic
"""

#-----------------------------------------------------------------------------
# Copyright (C) 2013 The IPython Development Team
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

import re

from .reader import get_version, versions

#-----------------------------------------------------------------------------
# Functions
#-----------------------------------------------------------------------------

def convert(nb, to_version):
"""Convert a notebook node object to a specific version. Assumes that
all the versions starting from 1 to the latest major X are implemented.
In other words, there should never be a case where v1 v2 v3 v5 exist without
a v4. Also assumes that all conversions can be made in one step increments
between major versions and ignores minor revisions.

PARAMETERS:
-----------
nb : NotebookNode
to_version : int
Major revision to convert the notebook to. Can either be an upgrade or
a downgrade.
"""

# Get input notebook version.
(version, version_minor) = get_version(nb)

# Check if destination is current version, if so return contents
if version == to_version:
return nb

# If the version exist, try to convert to it one step at a time.
elif to_version in versions:

# Get the the version that this recursion will convert to as a step
# closer to the final revision. Make sure the newer of the conversion
# functions is used to perform the conversion.
if to_version > version:
step_version = version + 1
convert_function = versions[step_version].upgrade
else:
step_version = version - 1
convert_function = versions[version].downgrade

# Convert and make sure version changed during conversion.
converted = convert_function(nb)
if converted.get('nbformat', 1) == version:
raise Exception("Cannot convert notebook from v%d to v%d. Operation" \
"failed silently." % (major, step_version))

# Recursively convert until target version is reached.
return convert(converted, to_version)
else:
raise Exception("Cannot convert notebook to v%d because that " \
"version doesn't exist" % (to_version))
50 changes: 12 additions & 38 deletions IPython/nbformat/current.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Authors:

* Brian Granger
* Jonathan Frederic
"""

#-----------------------------------------------------------------------------
Expand All @@ -17,21 +18,21 @@
#-----------------------------------------------------------------------------

from __future__ import print_function
import json

from xml.etree import ElementTree as ET
import re

from IPython.nbformat import v3
from IPython.nbformat import v2
from IPython.nbformat import v1

from IPython.nbformat.v3 import (
NotebookNode,
new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet,
parse_filename, new_metadata, new_author, new_heading_cell, nbformat,
nbformat_minor,
)

from .reader import reads as reader_reads
from .reader import versions
from .convert import convert

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
Expand All @@ -43,20 +44,6 @@
class NBFormatError(ValueError):
pass

class NotJSONError(ValueError):
pass


def parse_json(s, **kwargs):
"""Parse a string into a (nbformat, dict) tuple."""
try:
d = json.loads(s, **kwargs)
except ValueError:
raise NotJSONError("Notebook does not appear to be JSON: %r" % s[:16])
nbf = d.get('nbformat', 1)
nbm = d.get('nbformat_minor', 0)
return nbf, nbm, d


def parse_py(s, **kwargs):
"""Parse a string into a (nbformat, string) tuple."""
Expand All @@ -76,39 +63,26 @@ def parse_py(s, **kwargs):

def reads_json(s, **kwargs):
"""Read a JSON notebook from a string and return the NotebookNode object."""
nbf, minor, d = parse_json(s, **kwargs)
if nbf == 1:
nb = v1.to_notebook_json(d, **kwargs)
nb = v3.convert_to_this_nbformat(nb, orig_version=1)
elif nbf == 2:
nb = v2.to_notebook_json(d, **kwargs)
nb = v3.convert_to_this_nbformat(nb, orig_version=2)
elif nbf == 3:
nb = v3.to_notebook_json(d, **kwargs)
nb = v3.convert_to_this_nbformat(nb, orig_version=3, orig_minor=minor)
else:
raise NBFormatError('Unsupported JSON nbformat version %s (supported version: %i)' % (nbf, 3))
return nb
return convert(reader_reads(s), current_nbformat)


def writes_json(nb, **kwargs):
return v3.writes_json(nb, **kwargs)
return versions[current_nbformat].writes_json(nb, **kwargs)


def reads_py(s, **kwargs):
"""Read a .py notebook from a string and return the NotebookNode object."""
nbf, nbm, s = parse_py(s, **kwargs)
if nbf == 2:
nb = v2.to_notebook_py(s, **kwargs)
elif nbf == 3:
nb = v3.to_notebook_py(s, **kwargs)
if nbf in (2, 3):
nb = versions[nbf].to_notebook_py(s, **kwargs)
else:
raise NBFormatError('Unsupported PY nbformat version: %i' % nbf)
return nb


def writes_py(nb, **kwargs):
return v3.writes_py(nb, **kwargs)
# nbformat 3 is the latest format that supports py
return versions[3].writes_py(nb, **kwargs)


# High level API
Expand Down
107 changes: 107 additions & 0 deletions IPython/nbformat/reader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""API for reading notebooks.

Authors:

* Jonathan Frederic
"""

#-----------------------------------------------------------------------------
# Copyright (C) 2013 The IPython Development Team
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

import json

import v1
import v2
import v3

versions = {
1: v1,
2: v2,
3: v3,
}

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------

class NotJSONError(ValueError):
pass

def parse_json(s, **kwargs):
"""Parse a JSON string into a dict."""
try:
nb_dict = json.loads(s, **kwargs)
except ValueError:
# Limit the error message to 80 characters. Display whatever JSON will fit.
raise NotJSONError(("Notebook does not appear to be JSON: %r" % s)[:77] + "...")
return nb_dict

# High level API

def get_version(nb):
"""Get the version of a notebook.

Parameters
----------
nb : dict
NotebookNode or dict containing notebook data.

Returns
-------
Tuple containing major (int) and minor (int) version numbers
"""
major = nb.get('nbformat', 1)
minor = nb.get('nbformat_minor', 0)
return (major, minor)


def reads(s, **kwargs):
"""Read a notebook from a json string and return the
NotebookNode object.

This function properly reads notebooks of any version. No version
conversion is performed.

Parameters
----------
s : unicode
The raw unicode string to read the notebook from.

Returns
-------
nb : NotebookNode
The notebook that was read.
"""
nb_dict = parse_json(s, **kwargs)
(major, minor) = get_version(nb_dict)
if major in versions:
return versions[major].to_notebook_json(nb_dict, minor=minor)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you parse JSON, then re-pack it as JSON again?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not, it's poor name for that function (see current.py in master, it does the same thing). to_notebook_json is actually the to_notebook function in the vX\nbjson.py file (where vX = v2, 3 etc..) See snippet from code in master:

class JSONReader(NotebookReader):

    def reads(self, s, **kwargs):
        nb = json.loads(s, **kwargs)
        nb = self.to_notebook(nb, **kwargs)
        return nb

    def to_notebook(self, d, **kwargs):
        return rejoin_lines(from_dict(d))

It really is just rejoining the code lines. Should I rename this in this PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it is confusing, but I'm in favor of renaming things later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works for me, thanks.

else:
raise NBFormatError('Unsupported nbformat version %s' % major)


def read(fp, **kwargs):
"""Read a notebook from a file and return the NotebookNode object.

This function properly reads notebooks of any version. No version
conversion is performed.

Parameters
----------
fp : file
Any file-like object with a read method.

Returns
-------
nb : NotebookNode
The notebook that was read.
"""
return reads(fp.read(), **kwargs)
42 changes: 42 additions & 0 deletions IPython/nbformat/tests/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Contains base test class for nbformat
"""
#-----------------------------------------------------------------------------
#Copyright (c) 2013, the IPython Development Team.
#
#Distributed under the terms of the Modified BSD License.
#
#The full license is in the file COPYING.txt, distributed with this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

import os
import unittest

import IPython

#-----------------------------------------------------------------------------
# Classes and functions
#-----------------------------------------------------------------------------

class TestsBase(unittest.TestCase):
"""Base tests class."""

def fopen(self, f, mode=u'r'):
return open(os.path.join(self._get_files_path(), f), mode)


def _get_files_path(self):

#Get the relative path to this module in the IPython directory.
names = self.__module__.split(u'.')[1:-1]

#Build a path using the IPython directory and the relative path we just
#found.
path = IPython.__path__[0]
for name in names:
path = os.path.join(path, name)
return path