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
73 changes: 73 additions & 0 deletions IPython/nbformat/convert.py
@@ -0,0 +1,73 @@
"""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)
version_numbers = versions.keys()
Copy link
Member

Choose a reason for hiding this comment

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

no reason to call keys() here - you can do to_version in versions below.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks


# 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 version_numbers:

# 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) #todo
Copy link
Member

Choose a reason for hiding this comment

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

todo mark

Copy link
Member Author

Choose a reason for hiding this comment

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

Reminder to self to make sure that this maps the conversion methods. 😃

if converted.get('nbformat', 1) == version:
raise Exception("Cannot convert notebook from v%d to v%d. Operation" \
"failed silently." % (major, step_version))

# Recuresively convert until target version is reached.
Copy link
Member

Choose a reason for hiding this comment

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

Just a typo Recursively

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
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 == 2 or nbf == 3:
Copy link
Member

Choose a reason for hiding this comment

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

generally use in (2,3) for this

Copy link
Member Author

Choose a reason for hiding this comment

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

Neat trick, thanks for showing me that

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
106 changes: 106 additions & 0 deletions IPython/nbformat/reader.py
@@ -0,0 +1,106 @@
"""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:
raise NotJSONError("Notebook does not appear to be JSON: %r" % s[:16])
Copy link
Member

Choose a reason for hiding this comment

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

Why 16 in s[:16] ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just to display part of the JSON in the error message, that code came from current.py in master. Sorry it's kind of misleading since git has no way of tracking that I took those lines from that file.

See: https://github.com/ipython/ipython/blob/master/IPython/nbformat/current.py#L55

Just because it's in master doesn't mean it's the right behavior. It seems okay with me, do you think I should change it so it doesn't show any of the JSON?

Copy link
Member

Choose a reason for hiding this comment

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

Then just a comment that say that 16 is "random". That the kind of things you can wonder when you came across that later, like, it might be to avoid new line, you might want `len('Notebook does not appear to be JSON:')+n < 80' to fit on terminal screen..

And for aestetic purpose add a '...' , I guess we can suppose we will never be in a case where the file is smaller than 16 char or empty.

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, format='ipynb', **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

format should be 'json', not 'ipynb'

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 going to get rid of this, I think reader.py should only support json.

"""Read a notebook from a 'ipynb' (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(), format, **kwargs)
Copy link
Member

Choose a reason for hiding this comment

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

this will raise NameError on format

Since this function isn't even callable, that means you haven't added the bare minimum of tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, I haven't tested it all, or added tests. I was going to do that later if this general idea (having version conversion in nbformat) sounded good to you.

3 changes: 1 addition & 2 deletions IPython/nbformat/v1/__init__.py
Expand Up @@ -20,5 +20,4 @@
from .nbjson import reads as read_json, writes as write_json
from .nbjson import to_notebook as to_notebook_json

from .convert import convert_to_this_nbformat

from .convert import upgrade
2 changes: 1 addition & 1 deletion IPython/nbformat/v1/convert.py
Expand Up @@ -11,6 +11,6 @@
# Code
#-----------------------------------------------------------------------------

def convert_to_this_nbformat(nb, orig_version=None):
def upgrade(nb, orig_version=None):
raise ValueError('Cannot convert to v1 notebook format')

2 changes: 1 addition & 1 deletion IPython/nbformat/v2/__init__.py
Expand Up @@ -34,7 +34,7 @@
from .nbpy import reads as read_py, writes as write_py
from .nbpy import to_notebook as to_notebook_py

from .convert import convert_to_this_nbformat
from .convert import downgrade, upgrade

#-----------------------------------------------------------------------------
# Code
Expand Down
13 changes: 12 additions & 1 deletion IPython/nbformat/v2/convert.py
Expand Up @@ -3,6 +3,7 @@
Authors:

* Brian Granger
* Jonathan Frederic
"""

#-----------------------------------------------------------------------------
Expand All @@ -24,7 +25,7 @@
# Code
#-----------------------------------------------------------------------------

def convert_to_this_nbformat(nb, orig_version=1):
def upgrade(nb, from_version=1):
"""Convert a notebook to the v2 format.

Parameters
Expand All @@ -48,3 +49,13 @@ def convert_to_this_nbformat(nb, orig_version=1):
else:
raise ValueError('Cannot convert a notebook from v%s to v2' % orig_version)


def downgrade(nb):
"""Convert a v2 notebook to v1.

Parameters
----------
nb : NotebookNode
The Python representation of the notebook to convert.
"""
raise Exception("Downgrade from notebook v2 to v1 is not supported.")
2 changes: 1 addition & 1 deletion IPython/nbformat/v3/__init__.py
Expand Up @@ -30,7 +30,7 @@
from .nbpy import reads as read_py, writes as write_py
from .nbpy import to_notebook as to_notebook_py

from .convert import convert_to_this_nbformat
from .convert import downgrade, upgrade

#-----------------------------------------------------------------------------
# Code
Expand Down