diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index f0ca195..f63438a 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -355,6 +355,14 @@ class TrailingSlashHandler(web.RequestHandler): self.redirect(self.request.uri.rstrip('/')) #----------------------------------------------------------------------------- +# URL pattern fragments for re-use +#----------------------------------------------------------------------------- + +path_regex = r"(?P(?:/.*)*)" +notebook_name_regex = r"(?P[^/]+\.ipynb)" +notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex) + +#----------------------------------------------------------------------------- # URL to handler mappings #----------------------------------------------------------------------------- diff --git a/IPython/html/nbconvert/__init__.py b/IPython/html/nbconvert/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/IPython/html/nbconvert/__init__.py diff --git a/IPython/html/nbconvert/handlers.py b/IPython/html/nbconvert/handlers.py new file mode 100644 index 0000000..b7197a7 --- /dev/null +++ b/IPython/html/nbconvert/handlers.py @@ -0,0 +1,117 @@ +import io +import os +import zipfile + +from tornado import web + +from ..base.handlers import IPythonHandler, notebook_path_regex +from IPython.nbformat.current import to_notebook_json +from IPython.nbconvert.exporters.export import exporter_map +from IPython.utils import tz +from IPython.utils.py3compat import cast_bytes + +import sys + +def find_resource_files(output_files_dir): + files = [] + for dirpath, dirnames, filenames in os.walk(output_files_dir): + files.extend([os.path.join(dirpath, f) for f in filenames]) + return files + +def respond_zip(handler, name, output, resources): + """Zip up the output and resource files and respond with the zip file. + + Returns True if it has served a zip file, False if there are no resource + files, in which case we serve the plain output file. + """ + # Check if we have resource files we need to zip + output_files = resources.get('outputs', None) + if not output_files: + return False + + # Headers + zip_filename = os.path.splitext(name)[0] + '.zip' + handler.set_header('Content-Disposition', + 'attachment; filename="%s"' % zip_filename) + handler.set_header('Content-Type', 'application/zip') + + # Prepare the zip file + buffer = io.BytesIO() + zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED) + output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension'] + zipf.writestr(output_filename, cast_bytes(output, 'utf-8')) + for filename, data in output_files.items(): + zipf.writestr(os.path.basename(filename), data) + zipf.close() + + handler.finish(buffer.getvalue()) + return True + +class NbconvertFileHandler(IPythonHandler): + + SUPPORTED_METHODS = ('GET',) + + @web.authenticated + def get(self, format, path='', name=None): + exporter = exporter_map[format](config=self.config) + + path = path.strip('/') + os_path = self.notebook_manager.get_os_path(name, path) + if not os.path.isfile(os_path): + raise web.HTTPError(404, u'Notebook does not exist: %s' % name) + + info = os.stat(os_path) + self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime)) + + output, resources = exporter.from_filename(os_path) + + if respond_zip(self, name, output, resources): + return + + # Force download if requested + if self.get_argument('download', 'false').lower() == 'true': + filename = os.path.splitext(name)[0] + '.' + resources['output_extension'] + self.set_header('Content-Disposition', + 'attachment; filename="%s"' % filename) + + # MIME type + if exporter.output_mimetype: + self.set_header('Content-Type', + '%s; charset=utf-8' % exporter.output_mimetype) + + self.finish(output) + +class NbconvertPostHandler(IPythonHandler): + SUPPORTED_METHODS = ('POST',) + + @web.authenticated + def post(self, format): + exporter = exporter_map[format](config=self.config) + + model = self.get_json_body() + nbnode = to_notebook_json(model['content']) + + output, resources = exporter.from_notebook_node(nbnode) + + if respond_zip(self, nbnode.metadata.name, output, resources): + return + + # MIME type + if exporter.output_mimetype: + self.set_header('Content-Type', + '%s; charset=utf-8' % exporter.output_mimetype) + + self.finish(output) + +#----------------------------------------------------------------------------- +# URL to handler mappings +#----------------------------------------------------------------------------- + +_format_regex = r"(?P\w+)" + + +default_handlers = [ + (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex), + NbconvertFileHandler), + (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler), +] \ No newline at end of file diff --git a/IPython/html/nbconvert/tests/__init__.py b/IPython/html/nbconvert/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/IPython/html/nbconvert/tests/__init__.py diff --git a/IPython/html/nbconvert/tests/test_nbconvert_handlers.py b/IPython/html/nbconvert/tests/test_nbconvert_handlers.py new file mode 100644 index 0000000..16e44fe --- /dev/null +++ b/IPython/html/nbconvert/tests/test_nbconvert_handlers.py @@ -0,0 +1,120 @@ +# coding: utf-8 +import base64 +import io +import json +import os +from os.path import join as pjoin +import shutil + +import requests + +from IPython.html.utils import url_path_join +from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error +from IPython.nbformat.current import (new_notebook, write, new_worksheet, + new_heading_cell, new_code_cell, + new_output) + +class NbconvertAPI(object): + """Wrapper for nbconvert API calls.""" + def __init__(self, base_url): + self.base_url = base_url + + def _req(self, verb, path, body=None, params=None): + response = requests.request(verb, + url_path_join(self.base_url, 'nbconvert', path), + data=body, params=params, + ) + response.raise_for_status() + return response + + def from_file(self, format, path, name, download=False): + return self._req('GET', url_path_join(format, path, name), + params={'download':download}) + + def from_post(self, format, nbmodel): + body = json.dumps(nbmodel) + return self._req('POST', format, body) + + def list_formats(self): + return self._req('GET', '') + +png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00' +b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT' +b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82') + +class APITest(NotebookTestBase): + def setUp(self): + nbdir = self.notebook_dir.name + + if not os.path.isdir(pjoin(nbdir, 'foo')): + os.mkdir(pjoin(nbdir, 'foo')) + + nb = new_notebook(name='testnb') + + ws = new_worksheet() + nb.worksheets = [ws] + ws.cells.append(new_heading_cell(u'Created by test ³')) + cc1 = new_code_cell(input=u'print(2*6)') + cc1.outputs.append(new_output(output_text=u'12')) + cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout')) + ws.cells.append(cc1) + + with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w', + encoding='utf-8') as f: + write(nb, f, format='ipynb') + + self.nbconvert_api = NbconvertAPI(self.base_url()) + + def tearDown(self): + nbdir = self.notebook_dir.name + + for dname in ['foo']: + shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True) + + def test_from_file(self): + r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb') + self.assertEqual(r.status_code, 200) + self.assertIn(u'text/html', r.headers['Content-Type']) + self.assertIn(u'Created by test', r.text) + self.assertIn(u'print', r.text) + + r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb') + self.assertIn(u'text/x-python', r.headers['Content-Type']) + self.assertIn(u'print(2*6)', r.text) + + def test_from_file_404(self): + with assert_http_error(404): + self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb') + + def test_from_file_download(self): + r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True) + content_disposition = r.headers['Content-Disposition'] + self.assertIn('attachment', content_disposition) + self.assertIn('testnb.py', content_disposition) + + def test_from_file_zip(self): + r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True) + self.assertIn(u'application/zip', r.headers['Content-Type']) + self.assertIn(u'.zip', r.headers['Content-Disposition']) + + def test_from_post(self): + nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb') + nbmodel = requests.get(nbmodel_url).json() + + r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel) + self.assertEqual(r.status_code, 200) + self.assertIn(u'text/html', r.headers['Content-Type']) + self.assertIn(u'Created by test', r.text) + self.assertIn(u'print', r.text) + + r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel) + self.assertIn(u'text/x-python', r.headers['Content-Type']) + self.assertIn(u'print(2*6)', r.text) + + def test_from_post_zip(self): + nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb') + nbmodel = requests.get(nbmodel_url).json() + + r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel) + self.assertIn(u'application/zip', r.headers['Content-Type']) + self.assertIn(u'.zip', r.headers['Content-Disposition']) diff --git a/IPython/html/notebook/handlers.py b/IPython/html/notebook/handlers.py index f3174b7..e442d00 100644 --- a/IPython/html/notebook/handlers.py +++ b/IPython/html/notebook/handlers.py @@ -20,8 +20,7 @@ import os from tornado import web HTTPError = web.HTTPError -from ..base.handlers import IPythonHandler -from ..services.notebooks.handlers import _notebook_path_regex, _path_regex +from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex from ..utils import url_path_join, url_escape #----------------------------------------------------------------------------- @@ -85,7 +84,7 @@ class NotebookRedirectHandler(IPythonHandler): default_handlers = [ - (r"/notebooks%s" % _notebook_path_regex, NotebookHandler), - (r"/notebooks%s" % _path_regex, NotebookRedirectHandler), + (r"/notebooks%s" % notebook_path_regex, NotebookHandler), + (r"/notebooks%s" % path_regex, NotebookRedirectHandler), ] diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 5c68500..0dc0dc2 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -192,10 +192,12 @@ class NotebookWebApplication(web.Application): handlers.extend(load_handlers('auth.login')) handlers.extend(load_handlers('auth.logout')) handlers.extend(load_handlers('notebook.handlers')) + handlers.extend(load_handlers('nbconvert.handlers')) handlers.extend(load_handlers('services.kernels.handlers')) handlers.extend(load_handlers('services.notebooks.handlers')) handlers.extend(load_handlers('services.clusters.handlers')) handlers.extend(load_handlers('services.sessions.handlers')) + handlers.extend(load_handlers('services.nbconvert.handlers')) handlers.extend([ (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}), (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}), diff --git a/IPython/html/services/nbconvert/__init__.py b/IPython/html/services/nbconvert/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/IPython/html/services/nbconvert/__init__.py diff --git a/IPython/html/services/nbconvert/handlers.py b/IPython/html/services/nbconvert/handlers.py new file mode 100644 index 0000000..e2ced71 --- /dev/null +++ b/IPython/html/services/nbconvert/handlers.py @@ -0,0 +1,23 @@ +import json + +from tornado import web + +from ...base.handlers import IPythonHandler, json_errors +from IPython.nbconvert.exporters.export import exporter_map + +class NbconvertRootHandler(IPythonHandler): + SUPPORTED_METHODS = ('GET',) + + @web.authenticated + @json_errors + def get(self): + res = {} + for format, exporter in exporter_map.items(): + res[format] = info = {} + info['output_mimetype'] = exporter.output_mimetype + + self.finish(json.dumps(res)) + +default_handlers = [ + (r"/api/nbconvert", NbconvertRootHandler), +] \ No newline at end of file diff --git a/IPython/html/services/nbconvert/tests/__init__.py b/IPython/html/services/nbconvert/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/IPython/html/services/nbconvert/tests/__init__.py diff --git a/IPython/html/services/nbconvert/tests/test_nbconvert_api.py b/IPython/html/services/nbconvert/tests/test_nbconvert_api.py new file mode 100644 index 0000000..291ec74 --- /dev/null +++ b/IPython/html/services/nbconvert/tests/test_nbconvert_api.py @@ -0,0 +1,31 @@ +import requests + +from IPython.html.utils import url_path_join +from IPython.html.tests.launchnotebook import NotebookTestBase + +class NbconvertAPI(object): + """Wrapper for nbconvert API calls.""" + def __init__(self, base_url): + self.base_url = base_url + + def _req(self, verb, path, body=None, params=None): + response = requests.request(verb, + url_path_join(self.base_url, 'api/nbconvert', path), + data=body, params=params, + ) + response.raise_for_status() + return response + + def list_formats(self): + return self._req('GET', '') + +class APITest(NotebookTestBase): + def setUp(self): + self.nbconvert_api = NbconvertAPI(self.base_url()) + + def test_list_formats(self): + formats = self.nbconvert_api.list_formats().json() + self.assertIsInstance(formats, dict) + self.assertIn('python', formats) + self.assertIn('html', formats) + self.assertEqual(formats['python']['output_mimetype'], 'text/x-python') \ No newline at end of file diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index 3ae54ed..749892f 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -178,7 +178,7 @@ class FileNotebookManager(NotebookManager): return notebooks def get_notebook_model(self, name, path='', content=True): - """ Takes a path and name for a notebook and returns it's model + """ Takes a path and name for a notebook and returns its model Parameters ---------- diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/notebooks/handlers.py index 69d0ac4..27e74d7 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/notebooks/handlers.py @@ -23,7 +23,9 @@ from tornado import web from IPython.html.utils import url_path_join, url_escape from IPython.utils.jsonutil import date_default -from IPython.html.base.handlers import IPythonHandler, json_errors +from IPython.html.base.handlers import (IPythonHandler, json_errors, + notebook_path_regex, path_regex, + notebook_name_regex) #----------------------------------------------------------------------------- # Notebook web service handlers @@ -264,17 +266,14 @@ class ModifyNotebookCheckpointsHandler(IPythonHandler): #----------------------------------------------------------------------------- -_path_regex = r"(?P(?:/.*)*)" _checkpoint_id_regex = r"(?P[\w-]+)" -_notebook_name_regex = r"(?P[^/]+\.ipynb)" -_notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex) default_handlers = [ - (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler), - (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex), + (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler), + (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex), ModifyNotebookCheckpointsHandler), - (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler), - (r"/api/notebooks%s" % _path_regex, NotebookHandler), + (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler), + (r"/api/notebooks%s" % path_regex, NotebookHandler), ] diff --git a/IPython/html/static/notebook/js/celltoolbarpresets/rawcell.js b/IPython/html/static/notebook/js/celltoolbarpresets/rawcell.js index 3d51798..75965f0 100644 --- a/IPython/html/static/notebook/js/celltoolbarpresets/rawcell.js +++ b/IPython/html/static/notebook/js/celltoolbarpresets/rawcell.js @@ -22,7 +22,7 @@ ["reST", "text/restructuredtext"], ["HTML", "text/html"], ["Markdown", "text/markdown"], - ["Python", "application/x-python"], + ["Python", "text/x-python"], ["Custom", "dialog"], ], @@ -87,4 +87,4 @@ CellToolbar.register_preset('Raw Cell Format', raw_cell_preset); console.log('Raw Cell Format toolbar preset loaded.'); -}(IPython)); \ No newline at end of file +}(IPython)); diff --git a/IPython/html/static/notebook/js/menubar.js b/IPython/html/static/notebook/js/menubar.js index 7de2345..b4e8fec 100644 --- a/IPython/html/static/notebook/js/menubar.js +++ b/IPython/html/static/notebook/js/menubar.js @@ -69,6 +69,22 @@ var IPython = (function (IPython) { ); }; + MenuBar.prototype._nbconvert = function (format, download) { + download = download || false; + var notebook_name = IPython.notebook.get_notebook_name(); + if (IPython.notebook.dirty) { + IPython.notebook.save_notebook({async : false}); + } + var url = utils.url_path_join( + this.baseProjectUrl(), + 'nbconvert', + format, + this.notebookPath(), + notebook_name + '.ipynb' + ) + "?download=" + download.toString(); + + window.open(url); + } MenuBar.prototype.bind_events = function () { // File @@ -102,25 +118,22 @@ var IPython = (function (IPython) { window.location.assign(url); }); - /* FIXME: download-as-py doesn't work right now - * We will need nbconvert hooked up to get this back - + this.element.find('#print_preview').click(function () { + that._nbconvert('html', false); + }); + this.element.find('#download_py').click(function () { - var notebook_name = IPython.notebook.get_notebook_name(); - if (IPython.notebook.dirty) { - IPython.notebook.save_notebook({async : false}); - } - var url = utils.url_path_join( - that.baseProjectUrl(), - 'api/notebooks', - that.notebookPath(), - notebook_name + '.ipynb?format=py&download=True' - ); - window.location.assign(url); + that._nbconvert('python', true); }); - - */ - + + this.element.find('#download_html').click(function () { + that._nbconvert('html', true); + }); + + this.element.find('#download_rst').click(function () { + that._nbconvert('rst', true); + }); + this.element.find('#rename_notebook').click(function () { IPython.save_widget.rename_notebook(); }); diff --git a/IPython/html/templates/notebook.html b/IPython/html/templates/notebook.html index ae73841..84e8c37 100644 --- a/IPython/html/templates/notebook.html +++ b/IPython/html/templates/notebook.html @@ -77,10 +77,13 @@ class="notebook_app"
  • +
  • diff --git a/IPython/html/tests/launchnotebook.py b/IPython/html/tests/launchnotebook.py index f9a9058..4a78242 100644 --- a/IPython/html/tests/launchnotebook.py +++ b/IPython/html/tests/launchnotebook.py @@ -1,13 +1,14 @@ """Base class for notebook tests.""" -import os import sys import time import requests from contextlib import contextmanager -from subprocess import Popen, PIPE +from subprocess import Popen, STDOUT from unittest import TestCase +import nose + from IPython.utils.tempdir import TemporaryDirectory class NotebookTestBase(TestCase): @@ -55,10 +56,9 @@ class NotebookTestBase(TestCase): '--ipython-dir=%s' % cls.ipython_dir.name, '--notebook-dir=%s' % cls.notebook_dir.name, ] - devnull = open(os.devnull, 'w') cls.notebook = Popen(notebook_args, - stdout=devnull, - stderr=devnull, + stdout=nose.iptest_stdstreams_fileno(), + stderr=STDOUT, ) cls.wait_until_alive() diff --git a/IPython/html/tree/handlers.py b/IPython/html/tree/handlers.py index 1ec33d5..03820ca 100644 --- a/IPython/html/tree/handlers.py +++ b/IPython/html/tree/handlers.py @@ -18,9 +18,8 @@ Authors: import os from tornado import web -from ..base.handlers import IPythonHandler +from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex from ..utils import url_path_join, path2url, url2path, url_escape -from ..services.notebooks.handlers import _notebook_path_regex, _path_regex #----------------------------------------------------------------------------- # Handlers @@ -70,8 +69,8 @@ class TreeRedirectHandler(IPythonHandler): default_handlers = [ - (r"/tree%s" % _notebook_path_regex, TreeHandler), - (r"/tree%s" % _path_regex, TreeHandler), + (r"/tree%s" % notebook_path_regex, TreeHandler), + (r"/tree%s" % path_regex, TreeHandler), (r"/tree", TreeHandler), (r"/", TreeRedirectHandler), ] diff --git a/IPython/nbconvert/exporters/exporter.py b/IPython/nbconvert/exporters/exporter.py index 3a594d0..cc77fc7 100644 --- a/IPython/nbconvert/exporters/exporter.py +++ b/IPython/nbconvert/exporters/exporter.py @@ -53,6 +53,11 @@ class Exporter(LoggingConfigurable): help="Extension of the file that should be written to disk" ) + # MIME type of the result file, for HTTP response headers. + # This is *not* a traitlet, because we want to be able to access it from + # the class, not just on instances. + output_mimetype = '' + #Configurability, allows the user to easily add filters and preprocessors. preprocessors = List(config=True, help="""List of preprocessors, by name or namespace, to enable.""") diff --git a/IPython/nbconvert/exporters/html.py b/IPython/nbconvert/exporters/html.py index bb63397..8cb8675 100644 --- a/IPython/nbconvert/exporters/html.py +++ b/IPython/nbconvert/exporters/html.py @@ -36,11 +36,14 @@ class HTMLExporter(TemplateExporter): help="Extension of the file that should be written to disk" ) + mime_type = Unicode('text/html', config=True, + help="MIME type of the result file, for HTTP response headers." + ) + default_template = Unicode('full', config=True, help="""Flavor of the data format to use. I.E. 'full' or 'basic'""") - def _raw_mimetype_default(self): - return 'text/html' + output_mimetype = 'text/html' @property def default_config(self): diff --git a/IPython/nbconvert/exporters/latex.py b/IPython/nbconvert/exporters/latex.py index a095262..0a94b9d 100644 --- a/IPython/nbconvert/exporters/latex.py +++ b/IPython/nbconvert/exporters/latex.py @@ -63,8 +63,7 @@ class LatexExporter(TemplateExporter): #Extension that the template files use. template_extension = Unicode(".tplx", config=True) - def _raw_mimetype_default(self): - return 'text/latex' + output_mimetype = 'text/latex' @property diff --git a/IPython/nbconvert/exporters/markdown.py b/IPython/nbconvert/exporters/markdown.py index fb38539..6a888ae 100644 --- a/IPython/nbconvert/exporters/markdown.py +++ b/IPython/nbconvert/exporters/markdown.py @@ -30,11 +30,10 @@ class MarkdownExporter(TemplateExporter): 'md', config=True, help="Extension of the file that should be written to disk") - def _raw_mimetype_default(self): - return 'text/markdown' + output_mimetype = 'text/markdown' def _raw_mimetypes_default(self): - return ['text/markdown', 'text/html'] + return ['text/markdown', 'text/html', ''] @property def default_config(self): diff --git a/IPython/nbconvert/exporters/python.py b/IPython/nbconvert/exporters/python.py index 1d13bc3..b5a4293 100644 --- a/IPython/nbconvert/exporters/python.py +++ b/IPython/nbconvert/exporters/python.py @@ -29,6 +29,4 @@ class PythonExporter(TemplateExporter): 'py', config=True, help="Extension of the file that should be written to disk") - def _raw_mimetype_default(self): - return 'application/x-python' - + output_mimetype = 'text/x-python' diff --git a/IPython/nbconvert/exporters/rst.py b/IPython/nbconvert/exporters/rst.py index 22dfc82..e401c99 100644 --- a/IPython/nbconvert/exporters/rst.py +++ b/IPython/nbconvert/exporters/rst.py @@ -30,9 +30,8 @@ class RSTExporter(TemplateExporter): 'rst', config=True, help="Extension of the file that should be written to disk") - def _raw_mimetype_default(self): - return 'text/restructuredtext' - + output_mimetype = 'text/restructuredtext' + @property def default_config(self): c = Config({'ExtractOutputPreprocessor':{'enabled':True}}) diff --git a/IPython/nbconvert/exporters/slides.py b/IPython/nbconvert/exporters/slides.py index 6ecb5a9..1a4ebec 100644 --- a/IPython/nbconvert/exporters/slides.py +++ b/IPython/nbconvert/exporters/slides.py @@ -31,6 +31,8 @@ class SlidesExporter(HTMLExporter): help="Extension of the file that should be written to disk" ) + output_mimetype = 'text/html' + default_template = Unicode('reveal', config=True, help="""Template of the data format to use. I.E. 'reveal'""") diff --git a/IPython/nbconvert/exporters/templateexporter.py b/IPython/nbconvert/exporters/templateexporter.py index a5eed82..e127ab9 100644 --- a/IPython/nbconvert/exporters/templateexporter.py +++ b/IPython/nbconvert/exporters/templateexporter.py @@ -126,12 +126,11 @@ class TemplateExporter(Exporter): help="""Dictionary of filters, by name and namespace, to add to the Jinja environment.""") - raw_mimetype = Unicode('') raw_mimetypes = List(config=True, help="""formats of raw cells to be included in this Exporter's output.""" ) def _raw_mimetypes_default(self): - return [self.raw_mimetype] + return [self.output_mimetype, ''] def __init__(self, config=None, extra_loaders=None, **kw): @@ -209,7 +208,6 @@ class TemplateExporter(Exporter): preprocessors and filters. """ nb_copy, resources = super(TemplateExporter, self).from_notebook_node(nb, resources, **kw) - resources.setdefault('raw_mimetype', self.raw_mimetype) resources.setdefault('raw_mimetypes', self.raw_mimetypes) self._load_template() diff --git a/IPython/nbconvert/exporters/tests/base.py b/IPython/nbconvert/exporters/tests/base.py index 9db1caa..ad61321 100644 --- a/IPython/nbconvert/exporters/tests/base.py +++ b/IPython/nbconvert/exporters/tests/base.py @@ -23,7 +23,7 @@ from ...tests.base import TestsBase #----------------------------------------------------------------------------- all_raw_mimetypes = { - 'application/x-python', + 'text/x-python', 'text/markdown', 'text/html', 'text/restructuredtext', diff --git a/IPython/nbconvert/exporters/tests/files/rawtest.ipynb b/IPython/nbconvert/exporters/tests/files/rawtest.ipynb index 667ddbf..6eae33a 100644 --- a/IPython/nbconvert/exporters/tests/files/rawtest.ipynb +++ b/IPython/nbconvert/exporters/tests/files/rawtest.ipynb @@ -43,7 +43,7 @@ { "cell_type": "raw", "metadata": { - "raw_mimetype": "application/x-python" + "raw_mimetype": "text/x-python" }, "source": [ "def bar():\n", @@ -81,4 +81,4 @@ "metadata": {} } ] -} \ No newline at end of file +} diff --git a/IPython/nbconvert/preprocessors/extractoutput.py b/IPython/nbconvert/preprocessors/extractoutput.py index 961fde0..6fc5fee 100755 --- a/IPython/nbconvert/preprocessors/extractoutput.py +++ b/IPython/nbconvert/preprocessors/extractoutput.py @@ -17,7 +17,7 @@ import base64 import sys import os -from IPython.utils.traitlets import Unicode +from IPython.utils.traitlets import Unicode, Set from .base import Preprocessor from IPython.utils import py3compat @@ -34,6 +34,7 @@ class ExtractOutputPreprocessor(Preprocessor): output_filename_template = Unicode( "{unique_key}_{cell_index}_{index}.{extension}", config=True) + extract_output_types = Set({'png', 'jpg', 'svg', 'pdf'}, config=True) def preprocess_cell(self, cell, resources, cell_index): """ @@ -63,8 +64,8 @@ class ExtractOutputPreprocessor(Preprocessor): #Loop through all of the outputs in the cell for index, out in enumerate(cell.get('outputs', [])): - #Get the output in data formats that the template is interested in. - for out_type in self.display_data_priority: + #Get the output in data formats that the template needs extracted + for out_type in self.extract_output_types: if out.hasattr(out_type): data = out[out_type] diff --git a/IPython/nbconvert/preprocessors/tests/test_extractoutput.py b/IPython/nbconvert/preprocessors/tests/test_extractoutput.py index 08bd1f2..7c22fe5 100644 --- a/IPython/nbconvert/preprocessors/tests/test_extractoutput.py +++ b/IPython/nbconvert/preprocessors/tests/test_extractoutput.py @@ -29,6 +29,7 @@ class TestExtractOutput(PreprocessorTestsBase): def build_preprocessor(self): """Make an instance of a preprocessor""" preprocessor = ExtractOutputPreprocessor() + preprocessor.extract_output_types = {'text', 'png'} preprocessor.enabled = True return preprocessor diff --git a/IPython/nbconvert/templates/latex/skeleton/null.tplx b/IPython/nbconvert/templates/latex/skeleton/null.tplx index 5f8909b..2327e84 100644 --- a/IPython/nbconvert/templates/latex/skeleton/null.tplx +++ b/IPython/nbconvert/templates/latex/skeleton/null.tplx @@ -81,7 +81,7 @@ consider calling super even if it is a leave block, we might insert more blocks ((*- endblock headingcell -*)) ((*- elif cell.cell_type in ['raw'] -*)) ((*- block rawcell scoped -*)) - ((* if cell.metadata.get('raw_mimetype', resources.get('raw_mimetype')) == resources.get('raw_mimetype') *)) + ((* if cell.metadata.get('raw_mimetype', '').lower() in resources.get('raw_mimetypes', ['']) *)) ((( cell.source ))) ((* endif *)) ((*- endblock rawcell -*)) diff --git a/IPython/nbconvert/templates/python.tpl b/IPython/nbconvert/templates/python.tpl index c863da7..8aa456b 100644 --- a/IPython/nbconvert/templates/python.tpl +++ b/IPython/nbconvert/templates/python.tpl @@ -27,7 +27,7 @@ it introduces a new line {# .... #} {% block pyout %} -{{ output.text | indent | comment_lines }} +{{ output.text or '' | indent | comment_lines }} {% endblock pyout %} {% block stream %} @@ -48,4 +48,4 @@ it introduces a new line {% block unknowncell scoped %} unknown type {{ cell.type }} -{% endblock unknowncell %} \ No newline at end of file +{% endblock unknowncell %} diff --git a/IPython/nbconvert/templates/skeleton/null.tpl b/IPython/nbconvert/templates/skeleton/null.tpl index aec85f4..9779043 100644 --- a/IPython/nbconvert/templates/skeleton/null.tpl +++ b/IPython/nbconvert/templates/skeleton/null.tpl @@ -77,7 +77,7 @@ consider calling super even if it is a leave block, we might insert more blocks {%- endblock headingcell -%} {%- elif cell.cell_type in ['raw'] -%} {%- block rawcell scoped -%} - {% if cell.metadata.get('raw_mimetype', resources.get('raw_mimetype', '')).lower() in resources.get('raw_mimetypes', ['']) %} + {% if cell.metadata.get('raw_mimetype', '').lower() in resources.get('raw_mimetypes', ['']) %} {{ cell.source }} {% endif %} {%- endblock rawcell -%} diff --git a/IPython/nbformat/v3/nbbase.py b/IPython/nbformat/v3/nbbase.py index bec2447..c35d711 100644 --- a/IPython/nbformat/v3/nbbase.py +++ b/IPython/nbformat/v3/nbbase.py @@ -55,7 +55,8 @@ def new_output(output_type=None, output_text=None, output_png=None, output_html=None, output_svg=None, output_latex=None, output_json=None, output_javascript=None, output_jpeg=None, prompt_number=None, ename=None, evalue=None, traceback=None, stream=None, metadata=None): - """Create a new code cell with input and output""" + """Create a new output, to go in the ``cell.outputs`` list of a code cell. + """ output = NotebookNode() if output_type is not None: output.output_type = unicode_type(output_type) diff --git a/docs/source/whatsnew/pr/nbconvert-service.rst b/docs/source/whatsnew/pr/nbconvert-service.rst new file mode 100644 index 0000000..f3213c7 --- /dev/null +++ b/docs/source/whatsnew/pr/nbconvert-service.rst @@ -0,0 +1,3 @@ +* Print preview is back in the notebook menus, along with options to + download the open notebook in various formats. This is powered by + nbconvert.