-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
Changes from 6 commits
0851100
4590631
5dd1264
b23f5b1
4a52bac
8df1d7e
d872173
759d5e2
1411cb6
5bcf70b
5e5e132
7a5b0ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
|
||
# 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. todo mark There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a typo |
||
return convert(converted, to_version) | ||
else: | ||
raise Exception("Cannot convert notebook to v%d because that " \ | ||
"version doesn't exist" % (to_version)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
Authors: | ||
|
||
* Brian Granger | ||
* Jonathan Frederic | ||
""" | ||
|
||
#----------------------------------------------------------------------------- | ||
|
@@ -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 | ||
#----------------------------------------------------------------------------- | ||
|
@@ -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.""" | ||
|
@@ -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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. generally use There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why 16 in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then just a comment that say that 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. format should be 'json', not 'ipynb' There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would you parse JSON, then re-pack it as JSON again? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
There was a problem hiding this comment.
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 doto_version in versions
below.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks